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>
</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
});
}
}
+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.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