Merge pull request #32 from SlimusMinus/fix-image_moderation

fixed image moderation and dell redis config
This commit is contained in:
SlimusMinus
2026-09-07 01:39:52 +03:00
committed by GitHub
11 changed files with 49 additions and 64 deletions

View File

@@ -1,7 +1,6 @@
package com.krylov.refound.ai.client;
import com.krylov.refound.ai.dto.ImageModerationResponse;
import com.krylov.refound.ai.dto.ModerationResponse;
import com.krylov.refound.ai.exception.AiServerException;
import com.krylov.refound.ai.exception.AiUnavailableException;
import lombok.RequiredArgsConstructor;
@@ -29,12 +28,16 @@ public class ImageModerationClient {
}
@Recover
public ModerationResponse recover(AiUnavailableException e, String text) {
public ImageModerationResponse recover(AiUnavailableException e, MultipartFile file) {
log.error("AI ImageModeration unavailable after retries", e);
return ModerationResponse.builder()
.approved(false)
.reason("AI_UNAVAILABLE")
.build();
return null;
}
@Recover
public ImageModerationResponse recover(AiServerException e, MultipartFile file) {
log.error("AI ImageModeration server error after retries", e);
return null;
}
}

View File

@@ -32,9 +32,13 @@ public class TextModerationClient {
public ModerationResponse recover(AiUnavailableException e, String text) {
log.error("AI TextModeration unavailable after retries", e);
return ModerationResponse.builder()
.approved(false)
.reason("AI_UNAVAILABLE")
.build();
return null;
}
@Recover
public ModerationResponse recover(AiServerException e, String text) {
log.error("AI TextModeration server error after retries", e);
return null;
}
}

View File

@@ -15,7 +15,7 @@ public class ContentModerationFacade {
private final ContentModerationPipeline pipeline;
public void moderate(String title, String description, List<MultipartFile> images) {
public Boolean moderate(String title, String description, List<MultipartFile> images) {
log.info("moderate ContentModerationFacade get");
ModerationContext context =
ModerationContext.builder()
@@ -24,7 +24,7 @@ public class ContentModerationFacade {
.images(images)
.build();
pipeline.execute(context);
return pipeline.execute(context);
}
}

View File

@@ -12,11 +12,15 @@ public class ContentModerationPipeline {
private final List<ModerationProcessor> processors;
public void execute(ModerationContext context) {
public Boolean execute(ModerationContext context) {
log.info("execute ContentModerationPipeline");
for (ModerationProcessor processor : processors) {
processor.process(context);
Boolean approved = processor.process(context);
if (approved == null) {
return null;
}
}
return Boolean.TRUE;
}
}

View File

@@ -21,11 +21,11 @@ public class ImageModerationProcessor implements ModerationProcessor {
private final ImageModerationClient client;
@Override
public void process(ModerationContext context) {
public Boolean process(ModerationContext context) {
List<MultipartFile> images = context.getImages();
if (images == null || images.isEmpty()) {
return;
return Boolean.TRUE;
}
for (MultipartFile image : images) {
@@ -34,6 +34,10 @@ public class ImageModerationProcessor implements ModerationProcessor {
}
ImageModerationResponse response = client.moderate(image);
if (response == null) {
log.warn("AI ImageModeration вернул null, пропускаем модерацию изображения");
return null;
}
log.info("Image moderation result approved={}, score={}, reason={}, labels={}",
response.getApproved(),
@@ -42,10 +46,12 @@ public class ImageModerationProcessor implements ModerationProcessor {
response.getDetectedLabels()
);
if (!response.getApproved()) {
if (!Boolean.TRUE.equals(response.getApproved())) {
throw new ContentBlockedException(response.getReason(), response.getDetectedLabels());
}
}
return Boolean.TRUE;
}
}

View File

@@ -2,6 +2,6 @@ package com.krylov.refound.ai.pipeline;
public interface ModerationProcessor {
void process(ModerationContext context);
Boolean process(ModerationContext context);
}

View File

@@ -17,7 +17,7 @@ public class TextModerationProcessor implements ModerationProcessor {
private final TextModerationClient client;
@Override
public void process(ModerationContext context) {
public Boolean process(ModerationContext context) {
log.info("process TextModerationProcessor for context {}", context);
String text = buildText(
context.getTitle(),
@@ -25,12 +25,18 @@ public class TextModerationProcessor implements ModerationProcessor {
);
ModerationResponse response = client.moderate(text);
if (response == null) {
log.warn("AI TextModeration вернул null, пропускаем модерацию текста");
return null;
}
log.info("AI moderation result approved={}, score={}, reason={}", response.getApproved(), response.getScore(), response.getReason());
if (!response.getApproved()) {
if (!Boolean.TRUE.equals(response.getApproved())) {
throw new ContentBlockedException(response.getReason(), response.getDetectedLabels());
}
return Boolean.TRUE;
}
private String buildText(String title, String description) {

View File

@@ -1,10 +0,0 @@
package com.krylov.refound.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@EnableCaching
@Configuration
public class CacheConfig {
}

View File

@@ -1,27 +0,0 @@
package com.krylov.refound.config;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@Configuration
public class RedisConfig {
@Value("${cache.ttl}")
private long ttl;
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(ttl))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}

View File

@@ -44,7 +44,12 @@ public class PostModerationExecutorService {
.map(this::toMultipartFile)
.toList();
contentModerationFacade.moderate(post.getTitle(), post.getDescription(), images);
Boolean approved = contentModerationFacade.moderate(post.getTitle(), post.getDescription(), images);
if (approved == null) {
log.warn("Пост {} не отмодерирован из-за недоступности AI, статус не меняем", post.getId());
return;
}
postRepository.updateStatus(postId, PostStatus.ACTIVE);
log.info("Пост {} прошёл модерацию.", post.getId());

View File

@@ -23,9 +23,6 @@ spring:
host: localhost
port: 6379
cache:
type: redis
liquibase:
change-log: classpath:db/changelog/db.changelog-master.yaml
@@ -44,9 +41,6 @@ ai:
delay: 500
multiplier: 2
cache:
ttl: 300
jwt:
secret: ${JWT_SECRET:mySuperSecretKeyForJwtTokenGenerationThatIsLongEnough2024!ReFound}
access-expiration: ${JWT_ACCESS_EXPIRATION:900000} # 15 минут