From cd2230546c176bc167bd06f2e208db34ee2c6b07 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 3 Jan 2026 13:02:56 +0500 Subject: [PATCH] . --- pom.xml | 6 ++ .../konturai/parser/config/MongoConfig.java | 4 +- .../service/MarketingStrategyService.java | 15 ++++ .../NanoBananaImageGenerationService.java | 86 ++++++++++++++----- src/main/resources/application.properties | 9 +- 5 files changed, 95 insertions(+), 25 deletions(-) diff --git a/pom.xml b/pom.xml index b989bd1..f426642 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,12 @@ jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + org.apache.xmlgraphics batik-codec diff --git a/src/main/java/kz/konturai/parser/config/MongoConfig.java b/src/main/java/kz/konturai/parser/config/MongoConfig.java index 2e5ace0..fa5951d 100644 --- a/src/main/java/kz/konturai/parser/config/MongoConfig.java +++ b/src/main/java/kz/konturai/parser/config/MongoConfig.java @@ -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() { diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java index c1983f7..7aa6e1e 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyService.java @@ -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); } diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index 3c26489..e893d61 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -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> candidates = (List>) 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 }); } } - diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1cf3073..fb9e4c9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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