This commit is contained in:
root
2026-01-03 13:02:56 +05:00
parent ec87821589
commit cd2230546c
5 changed files with 95 additions and 25 deletions
+6
View File
@@ -114,6 +114,12 @@
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<!-- Jackson JSR310 module for Java 8 time types -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.xmlgraphics</groupId> <groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId> <artifactId>batik-codec</artifactId>
@@ -2,6 +2,7 @@ package kz.konturai.parser.config;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.bson.Document; import org.bson.Document;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -21,7 +22,8 @@ import java.util.Map;
public class MongoConfig { public class MongoConfig {
private static final Logger logger = LoggerFactory.getLogger(MongoConfig.class); private static final Logger logger = LoggerFactory.getLogger(MongoConfig.class);
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
@Bean @Bean
public MongoCustomConversions customConversions() { public MongoCustomConversions customConversions() {
@@ -13,6 +13,7 @@ import kz.konturai.parser.repository.MarketingAnalysisRepository;
import kz.konturai.parser.repository.MarketingStrategyRepository; import kz.konturai.parser.repository.MarketingStrategyRepository;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -36,6 +37,9 @@ public class MarketingStrategyService {
private final PostingTaskService postingTaskService; private final PostingTaskService postingTaskService;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${image.generation.delayBetweenRequestsMs:3000}")
private long delayBetweenRequestsMs;
public MarketingStrategyService( public MarketingStrategyService(
MarketingStrategyRepository repository, MarketingStrategyRepository repository,
MarketingAnalysisService marketingAnalysisService, MarketingAnalysisService marketingAnalysisService,
@@ -432,6 +436,17 @@ public class MarketingStrategyService {
// Generate image for the post // Generate image for the post
generateAndSaveImageForPost(item, businessContext); generateAndSaveImageForPost(item, businessContext);
// Add delay between image generation requests to avoid rate limiting
// This helps prevent 429 errors from Gemini API
if (delayBetweenRequestsMs > 0) {
try {
Thread.sleep(delayBetweenRequestsMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Image generation delay interrupted");
}
}
calendar.add(item); calendar.add(item);
} }
@@ -50,11 +50,15 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
@Value("${google.gemini.retry.multiplier:2.0}") @Value("${google.gemini.retry.multiplier:2.0}")
private double retryMultiplier; private double retryMultiplier;
@Value("${google.gemini.rateLimit.delayMs:2000}")
private long rateLimitDelayMs;
public NanoBananaImageGenerationService() { public NanoBananaImageGenerationService() {
HttpClient httpClient = HttpClient.create() HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofMillis(90000)); .responseTimeout(Duration.ofMillis(90000));
// Configure exchange strategies to increase buffer limit for large base64 responses // Configure exchange strategies to increase buffer limit for large base64
// responses
ExchangeStrategies strategies = ExchangeStrategies.builder() ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10 MB .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10 MB
.build(); .build();
@@ -217,7 +221,8 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
} }
/** /**
* Обогащает промпт для Gemini, добавляя инструкции для корректной генерации изображений * Обогащает промпт для Gemini, добавляя инструкции для корректной генерации
* изображений
* и правильного отображения русского текста * и правильного отображения русского текста
* *
* @param originalPrompt Исходный промпт от пользователя * @param originalPrompt Исходный промпт от пользователя
@@ -268,7 +273,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
long attempt = retrySignal.totalRetries() + 1; long attempt = retrySignal.totalRetries() + 1;
Throwable failure = retrySignal.failure(); Throwable failure = retrySignal.failure();
Duration retryAfter = null;
if (failure instanceof WebClientResponseException) { if (failure instanceof WebClientResponseException) {
WebClientResponseException wcre = (WebClientResponseException) failure; WebClientResponseException wcre = (WebClientResponseException) failure;
if (wcre.getStatusCode().value() == 429) { if (wcre.getStatusCode().value() == 429) {
@@ -276,19 +280,58 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
if (retryAfterHeader != null) { if (retryAfterHeader != null) {
try { try {
int seconds = Integer.parseInt(retryAfterHeader); int seconds = Integer.parseInt(retryAfterHeader);
retryAfter = Duration.ofSeconds(seconds);
logger.warn( logger.warn(
"Gemini API returned 429 for operation '{}'. Retry-After: {} seconds. Will retry in {}ms (attempt {}/{})", "Gemini API returned 429 for operation '{}'. Retry-After: {} seconds (attempt {}/{})",
operation, seconds, retryAfter.toMillis(), attempt, maxRetryAttempts); operation, seconds, attempt, maxRetryAttempts);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// Ignore if header is not a number logger.warn(
} "Gemini API returned 429 for operation '{}'. Invalid Retry-After header: {} (attempt {}/{})",
operation, retryAfterHeader, attempt, maxRetryAttempts);
} }
} else {
logger.warn(
"Gemini API returned 429 for operation '{}'. No Retry-After header. Using backoff delay (attempt {}/{})",
operation, attempt, maxRetryAttempts);
} }
} else {
// Log all available error information
StringBuilder errorInfo = new StringBuilder();
errorInfo.append("Gemini API returned error ").append(wcre.getStatusCode().value())
.append(" (").append(wcre.getStatusCode().toString()).append(")");
errorInfo.append(" for operation '").append(operation).append("'");
errorInfo.append(" (attempt ").append(attempt).append("/").append(maxRetryAttempts)
.append(")");
// Add request URL if available
if (wcre.getRequest() != null && wcre.getRequest().getURI() != null) {
errorInfo.append("\nRequest URL: ").append(wcre.getRequest().getURI());
} }
if (retryAfter == null) { // Add response headers
logger.warn("Gemini API returned error for operation '{}'. Will retry (attempt {}/{})", if (wcre.getHeaders() != null && !wcre.getHeaders().isEmpty()) {
errorInfo.append("\nResponse headers: ").append(wcre.getHeaders());
}
// Add response body if available
try {
String responseBody = wcre.getResponseBodyAsString();
if (responseBody != null && !responseBody.isEmpty()) {
errorInfo.append("\nResponse body: ").append(responseBody);
}
} catch (Exception e) {
errorInfo.append("\nResponse body: [unable to read: ").append(e.getMessage())
.append("]");
}
// Add error message
if (wcre.getMessage() != null) {
errorInfo.append("\nError message: ").append(wcre.getMessage());
}
logger.warn(errorInfo.toString());
}
} else {
logger.warn("Network error for operation '{}'. Will retry (attempt {}/{})",
operation, attempt, maxRetryAttempts); operation, attempt, maxRetryAttempts);
} }
}) })
@@ -299,4 +342,3 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
}); });
} }
} }
+7 -2
View File
@@ -100,10 +100,15 @@ google.gemini.api.key=AIzaSyAHv_R0KIk7WyZLn1tL4MglrTepsjQWT_k
google.gemini.image.model=gemini-2.5-flash-image google.gemini.image.model=gemini-2.5-flash-image
google.gemini.timeoutMs=90000 google.gemini.timeoutMs=90000
google.gemini.retry.maxAttempts=3 google.gemini.retry.maxAttempts=3
google.gemini.retry.initialDelayMs=2000 google.gemini.retry.initialDelayMs=10000
google.gemini.retry.maxDelayMs=60000 google.gemini.retry.maxDelayMs=120000
google.gemini.retry.multiplier=2.0 google.gemini.retry.multiplier=2.0
# Delay between image generation requests (in milliseconds)
# Helps prevent rate limiting when generating multiple images sequentially
# Set to 0 to disable delay
image.generation.delayBetweenRequestsMs=3000
# Email Configuration # Email Configuration
spring.mail.host=smtp.gmail.com spring.mail.host=smtp.gmail.com
spring.mail.port=587 spring.mail.port=587