Merge remote-tracking branch 'refs/remotes/origin/analyze-graphics' into analyze-graphics

This commit is contained in:
root
2026-01-07 21:06:40 +05:00
3 changed files with 74 additions and 37 deletions
+6 -2
View File
@@ -42,8 +42,12 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Data MongoDB Starter -->
<dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.23.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
@@ -1,5 +1,8 @@
package kz.konturai.parser.service;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import io.netty.resolver.DefaultAddressResolverGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@@ -9,10 +12,11 @@ import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
@@ -24,15 +28,19 @@ import java.util.concurrent.Semaphore;
public class NanoBananaImageGenerationService implements ImageGenerationService {
private static final Logger logger = LoggerFactory.getLogger(NanoBananaImageGenerationService.class);
// Базовый URL остался, но эндпоинты могут отличаться
private static final String GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/";
// Шаблон URL для Vertex AI (не Generative Language!)
private static final String VERTEX_API_TEMPLATE = "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predict";
private final Semaphore semaphore = new Semaphore(1);
private volatile long lastRequestTimestamp = 0;
private final WebClient webClient;
@Value("${google.gemini.api.key:}")
private String apiKey;
@Value("${google.cloud.project-id}")
private String projectId;
@Value("${google.cloud.location:us-central1}")
private String location;
@Value("${google.gemini.image.model:imagen-3.0-generate-001}")
private String model;
@@ -48,14 +56,15 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
public NanoBananaImageGenerationService() {
HttpClient httpClient = HttpClient.create()
.resolver(DefaultAddressResolverGroup.INSTANCE)
.responseTimeout(Duration.ofMillis(120000));
// Увеличиваем буфер памяти для приема больших картинок (Base64)
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(15 * 1024 * 1024)) // Увеличим до 15MB
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(20 * 1024 * 1024)) // 20MB
.build();
this.webClient = WebClient.builder()
.baseUrl(GEMINI_API_BASE_URL)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.exchangeStrategies(strategies)
.build();
@@ -63,8 +72,8 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
@Override
public byte[] generateImage(String prompt) {
if (apiKey == null || apiKey.trim().isEmpty()) {
logger.error("API Key is missing!");
if (projectId == null || projectId.trim().isEmpty()) {
logger.error("Project ID is missing! Check application.properties");
return null;
}
@@ -76,41 +85,63 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
Thread.sleep(rateLimitDelayMs - timeSinceLast);
}
String accessToken = getAccessToken();
if (accessToken == null) {
logger.error("Failed to get Access Token. Check 'gcloud auth login' or JSON key file.");
return null;
}
// 2. Формируем URL для Vertex AI
// Пример: https://us-central1-aiplatform.googleapis.com/...
String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model);
String enrichedPrompt = enrichPromptForGemini(prompt);
String endpoint = model + ":predict";
Map<String, Object> requestBody = buildImagenRequestBody(enrichedPrompt);
logger.info("Sending request to Imagen model: {}", model);
logger.info("Sending request to Vertex AI. Project: {}, Model: {}", projectId, model);
// 3. Выполняем POST запрос
Map<String, Object> response = webClient.post()
.uri(uriBuilder -> uriBuilder
.path(endpoint)
.queryParam("key", apiKey)
.build())
.uri(URI.create(endpointUrl))
.header("Authorization", "Bearer " + accessToken) // Авторизация через токен
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.retryWhen(createRetrySpecFor429("generateImage"))
.doOnError(e -> logger.error("API Error: {}", e.getMessage()))
.retryWhen(createRetrySpecFor429())
.doOnError(e -> {
logger.error("Vertex AI API Error: {}", e.getMessage());
if (e instanceof WebClientResponseException) {
logger.error("Response Body: {}", ((WebClientResponseException) e).getResponseBodyAsString());
}
})
.block(Duration.ofMillis(timeoutMs));
lastRequestTimestamp = System.currentTimeMillis();
return extractImageFromImagenResponse(response);
} catch (Exception e) {
logger.error("Fatal error in NanoBanana: {}", e.getMessage(), e);
logger.error("Fatal error in NanoBanana (Vertex AI): {}", e.getMessage(), e);
return null;
} finally {
semaphore.release();
}
}
/**
* Специфичный Request Body для модели Imagen
*/
private String getAccessToken() {
try {
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault()
.createScoped("https://www.googleapis.com/auth/cloud-platform");
credentials.refreshIfExpired();
AccessToken token = credentials.getAccessToken();
return token.getTokenValue();
} catch (IOException e) {
logger.error("Error obtaining Google Credentials", e);
return null;
}
}
private Map<String, Object> buildImagenRequestBody(String prompt) {
Map<String, Object> body = new HashMap<>();
@@ -118,13 +149,11 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
Map<String, Object> instance = new HashMap<>();
instance.put("prompt", prompt);
instances.add(instance);
body.put("instances", instances);
Map<String, Object> parameters = new HashMap<>();
parameters.put("sampleCount", 1); // Генерируем 1 картинку
parameters.put("aspectRatio", "1:1"); // Можно вынести в настройки
// parameters.put("personGeneration", "allow_adult"); // Если нужно (зависит от прав API ключа)
parameters.put("sampleCount", 1); // Количество картинок
parameters.put("aspectRatio", "1:1"); // Пропорции
body.put("parameters", parameters);
return body;
@@ -139,9 +168,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
if (predictions != null && !predictions.isEmpty()) {
Map<String, Object> firstPrediction = predictions.get(0);
// Imagen на Vertex AI обычно отдает поле "bytesBase64Encoded"
String base64Image = (String) firstPrediction.get("bytesBase64Encoded");
if (base64Image == null) {
// Резервный вариант, если формат API изменится
base64Image = (String) firstPrediction.get("b64");
}
@@ -149,14 +181,14 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
return java.util.Base64.getDecoder().decode(base64Image);
}
}
logger.warn("No 'predictions' or base64 data found in response: {}", response);
logger.warn("No image data found in Vertex AI response. Response: {}", response);
} catch (Exception e) {
logger.error("Parsing error: {}", e.getMessage());
}
return null;
}
private reactor.util.retry.RetryBackoffSpec createRetrySpecFor429(String operation) {
private reactor.util.retry.RetryBackoffSpec createRetrySpecFor429() {
return reactor.util.retry.Retry.backoff(maxRetryAttempts, Duration.ofSeconds(5))
.maxBackoff(Duration.ofSeconds(30))
.filter(t -> t instanceof WebClientResponseException &&
@@ -164,6 +196,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
}
private String enrichPromptForGemini(String original) {
return "High quality, photorealistic image of: " + original;
return "high quality, photorealistic image of: " + original;
}
}
+6 -5
View File
@@ -94,13 +94,14 @@ openai.image.style=natural
# Set to 'nano-banana' to use Google Gemini Nano Banana for image generation
image.generation.provider=nano-banana
# Google Gemini Nano Banana Image Generation Configuration
# Get API key from: https://aistudio.google.com/
google.gemini.api.key=AIzaSyDMhORdwYaqlXUDP--mmxXUWozFxMAp2hY
google.gemini.image.model=imagegeneration@006
google.cloud.project-id=onyx-yeti-456518-d4
google.cloud.location=us-central1
google.gemini.image.model=imagen-3.0-generate-001
google.gemini.timeoutMs=120000
google.gemini.retry.maxAttempts=3
google.gemini.retry.initialDelayMs=10000
\google.gemini.retry.initialDelayMs=10000
google.gemini.retry.maxDelayMs=120000
google.gemini.retry.multiplier=2.0
google.gemini.rateLimit.delayMs=12000