From 373b2c576e941680d35ee228eed5229ad87a305d Mon Sep 17 00:00:00 2001 From: arys Date: Wed, 7 Jan 2026 20:46:29 +0500 Subject: [PATCH 1/3] com --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ea95693..bbb6ab3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -97,7 +97,7 @@ 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.gemini.image.model=imagen-3.0-generate-001 google.gemini.timeoutMs=120000 google.gemini.retry.maxAttempts=3 google.gemini.retry.initialDelayMs=10000 From 10529a740fc54af66b7fdead46c6835c99007333 Mon Sep 17 00:00:00 2001 From: arys Date: Wed, 7 Jan 2026 20:55:16 +0500 Subject: [PATCH 2/3] com --- .../parser/service/NanoBananaImageGenerationService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index dfe5599..e40e237 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -1,5 +1,6 @@ package kz.konturai.parser.service; +import io.netty.resolver.DefaultAddressResolverGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -48,6 +49,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService public NanoBananaImageGenerationService() { HttpClient httpClient = HttpClient.create() + .resolver(DefaultAddressResolverGroup.INSTANCE) .responseTimeout(Duration.ofMillis(120000)); ExchangeStrategies strategies = ExchangeStrategies.builder() From d4bb6f01adb9d7e7f7ebde5e35acd0945812edef Mon Sep 17 00:00:00 2001 From: arys Date: Wed, 7 Jan 2026 21:05:49 +0500 Subject: [PATCH 3/3] com --- pom.xml | 8 +- .../NanoBananaImageGenerationService.java | 90 ++++++++++++------- src/main/resources/application.properties | 9 +- 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/pom.xml b/pom.xml index f426642..7ba95c6 100644 --- a/pom.xml +++ b/pom.xml @@ -42,8 +42,12 @@ spring-boot-starter-web - - + + com.google.auth + google-auth-library-oauth2-http + 1.23.0 + + org.springframework.boot spring-boot-starter-data-mongodb diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index e40e237..97d130c 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -1,5 +1,7 @@ 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; @@ -10,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; @@ -25,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; @@ -52,12 +59,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService .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(); @@ -65,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; } @@ -78,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 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 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>() {}) - .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 buildImagenRequestBody(String prompt) { Map body = new HashMap<>(); @@ -120,13 +149,11 @@ public class NanoBananaImageGenerationService implements ImageGenerationService Map instance = new HashMap<>(); instance.put("prompt", prompt); instances.add(instance); - body.put("instances", instances); Map 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; @@ -141,9 +168,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService if (predictions != null && !predictions.isEmpty()) { Map firstPrediction = predictions.get(0); + + // Imagen на Vertex AI обычно отдает поле "bytesBase64Encoded" String base64Image = (String) firstPrediction.get("bytesBase64Encoded"); if (base64Image == null) { + // Резервный вариант, если формат API изменится base64Image = (String) firstPrediction.get("b64"); } @@ -151,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 && @@ -166,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; } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bbb6ab3..3d1cab1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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.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