.
This commit is contained in:
@@ -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();
|
||||||
@@ -164,16 +168,16 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
|
|||||||
try {
|
try {
|
||||||
// Структура ответа Gemini API:
|
// Структура ответа Gemini API:
|
||||||
// {
|
// {
|
||||||
// "candidates": [{
|
// "candidates": [{
|
||||||
// "content": {
|
// "content": {
|
||||||
// "parts": [{
|
// "parts": [{
|
||||||
// "inlineData": {
|
// "inlineData": {
|
||||||
// "mimeType": "image/png",
|
// "mimeType": "image/png",
|
||||||
// "data": "base64_encoded_image_data"
|
// "data": "base64_encoded_image_data"
|
||||||
// }
|
// }
|
||||||
// }]
|
// }]
|
||||||
// }
|
// }
|
||||||
// }]
|
// }]
|
||||||
// }
|
// }
|
||||||
|
|
||||||
List<Map<String, Object>> candidates = (List<Map<String, Object>>) response.get("candidates");
|
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 Исходный промпт от пользователя
|
* @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(")");
|
||||||
|
|
||||||
if (retryAfter == null) {
|
// Add request URL if available
|
||||||
logger.warn("Gemini API returned error for operation '{}'. Will retry (attempt {}/{})",
|
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);
|
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.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
|
||||||
|
|||||||
Reference in New Issue
Block a user