.
This commit is contained in:
@@ -114,6 +114,12 @@
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson JSR310 module for Java 8 time types -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.xmlgraphics</groupId>
|
||||
<artifactId>batik-codec</artifactId>
|
||||
|
||||
@@ -2,6 +2,7 @@ package kz.konturai.parser.config;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.bson.Document;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -21,7 +22,8 @@ import java.util.Map;
|
||||
public class MongoConfig {
|
||||
|
||||
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
|
||||
public MongoCustomConversions customConversions() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import kz.konturai.parser.repository.MarketingAnalysisRepository;
|
||||
import kz.konturai.parser.repository.MarketingStrategyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -36,6 +37,9 @@ public class MarketingStrategyService {
|
||||
private final PostingTaskService postingTaskService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Value("${image.generation.delayBetweenRequestsMs:3000}")
|
||||
private long delayBetweenRequestsMs;
|
||||
|
||||
public MarketingStrategyService(
|
||||
MarketingStrategyRepository repository,
|
||||
MarketingAnalysisService marketingAnalysisService,
|
||||
@@ -432,6 +436,17 @@ public class MarketingStrategyService {
|
||||
// Generate image for the post
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,15 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
@Value("${google.gemini.retry.multiplier:2.0}")
|
||||
private double retryMultiplier;
|
||||
|
||||
@Value("${google.gemini.rateLimit.delayMs:2000}")
|
||||
private long rateLimitDelayMs;
|
||||
|
||||
public NanoBananaImageGenerationService() {
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.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()
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) // 10 MB
|
||||
.build();
|
||||
@@ -164,16 +168,16 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
try {
|
||||
// Структура ответа Gemini API:
|
||||
// {
|
||||
// "candidates": [{
|
||||
// "content": {
|
||||
// "parts": [{
|
||||
// "inlineData": {
|
||||
// "mimeType": "image/png",
|
||||
// "data": "base64_encoded_image_data"
|
||||
// }
|
||||
// }]
|
||||
// }
|
||||
// }]
|
||||
// "candidates": [{
|
||||
// "content": {
|
||||
// "parts": [{
|
||||
// "inlineData": {
|
||||
// "mimeType": "image/png",
|
||||
// "data": "base64_encoded_image_data"
|
||||
// }
|
||||
// }]
|
||||
// }
|
||||
// }]
|
||||
// }
|
||||
|
||||
List<Map<String, Object>> candidates = (List<Map<String, Object>>) response.get("candidates");
|
||||
@@ -217,7 +221,8 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
}
|
||||
|
||||
/**
|
||||
* Обогащает промпт для Gemini, добавляя инструкции для корректной генерации изображений
|
||||
* Обогащает промпт для Gemini, добавляя инструкции для корректной генерации
|
||||
* изображений
|
||||
* и правильного отображения русского текста
|
||||
*
|
||||
* @param originalPrompt Исходный промпт от пользователя
|
||||
@@ -268,7 +273,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
long attempt = retrySignal.totalRetries() + 1;
|
||||
Throwable failure = retrySignal.failure();
|
||||
|
||||
Duration retryAfter = null;
|
||||
if (failure instanceof WebClientResponseException) {
|
||||
WebClientResponseException wcre = (WebClientResponseException) failure;
|
||||
if (wcre.getStatusCode().value() == 429) {
|
||||
@@ -276,19 +280,58 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
if (retryAfterHeader != null) {
|
||||
try {
|
||||
int seconds = Integer.parseInt(retryAfterHeader);
|
||||
retryAfter = Duration.ofSeconds(seconds);
|
||||
logger.warn(
|
||||
"Gemini API returned 429 for operation '{}'. Retry-After: {} seconds. Will retry in {}ms (attempt {}/{})",
|
||||
operation, seconds, retryAfter.toMillis(), attempt, maxRetryAttempts);
|
||||
"Gemini API returned 429 for operation '{}'. Retry-After: {} seconds (attempt {}/{})",
|
||||
operation, seconds, attempt, maxRetryAttempts);
|
||||
} 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(")");
|
||||
|
||||
if (retryAfter == null) {
|
||||
logger.warn("Gemini API returned error for operation '{}'. Will retry (attempt {}/{})",
|
||||
// Add request URL if available
|
||||
if (wcre.getRequest() != null && wcre.getRequest().getURI() != null) {
|
||||
errorInfo.append("\nRequest URL: ").append(wcre.getRequest().getURI());
|
||||
}
|
||||
|
||||
// Add response headers
|
||||
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);
|
||||
}
|
||||
})
|
||||
@@ -299,4 +342,3 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,10 +100,15 @@ google.gemini.api.key=AIzaSyAHv_R0KIk7WyZLn1tL4MglrTepsjQWT_k
|
||||
google.gemini.image.model=gemini-2.5-flash-image
|
||||
google.gemini.timeoutMs=90000
|
||||
google.gemini.retry.maxAttempts=3
|
||||
google.gemini.retry.initialDelayMs=2000
|
||||
google.gemini.retry.maxDelayMs=60000
|
||||
google.gemini.retry.initialDelayMs=10000
|
||||
google.gemini.retry.maxDelayMs=120000
|
||||
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
|
||||
spring.mail.host=smtp.gmail.com
|
||||
spring.mail.port=587
|
||||
|
||||
Reference in New Issue
Block a user