This commit is contained in:
arys
2026-01-07 21:05:49 +05:00
parent 10529a740f
commit d4bb6f01ad
3 changed files with 71 additions and 36 deletions
+6 -2
View File
@@ -42,8 +42,12 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </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> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId> <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency> </dependency>
@@ -1,5 +1,7 @@
package kz.konturai.parser.service; package kz.konturai.parser.service;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import io.netty.resolver.DefaultAddressResolverGroup; import io.netty.resolver.DefaultAddressResolverGroup;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import java.io.IOException;
import java.net.URI;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -25,15 +28,19 @@ import java.util.concurrent.Semaphore;
public class NanoBananaImageGenerationService implements ImageGenerationService { public class NanoBananaImageGenerationService implements ImageGenerationService {
private static final Logger logger = LoggerFactory.getLogger(NanoBananaImageGenerationService.class); 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 final Semaphore semaphore = new Semaphore(1);
private volatile long lastRequestTimestamp = 0; private volatile long lastRequestTimestamp = 0;
private final WebClient webClient; private final WebClient webClient;
@Value("${google.gemini.api.key:}") @Value("${google.cloud.project-id}")
private String apiKey; private String projectId;
@Value("${google.cloud.location:us-central1}")
private String location;
@Value("${google.gemini.image.model:imagen-3.0-generate-001}") @Value("${google.gemini.image.model:imagen-3.0-generate-001}")
private String model; private String model;
@@ -52,12 +59,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
.resolver(DefaultAddressResolverGroup.INSTANCE) .resolver(DefaultAddressResolverGroup.INSTANCE)
.responseTimeout(Duration.ofMillis(120000)); .responseTimeout(Duration.ofMillis(120000));
// Увеличиваем буфер памяти для приема больших картинок (Base64)
ExchangeStrategies strategies = ExchangeStrategies.builder() ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(15 * 1024 * 1024)) // Увеличим до 15MB .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(20 * 1024 * 1024)) // 20MB
.build(); .build();
this.webClient = WebClient.builder() this.webClient = WebClient.builder()
.baseUrl(GEMINI_API_BASE_URL)
.clientConnector(new ReactorClientHttpConnector(httpClient)) .clientConnector(new ReactorClientHttpConnector(httpClient))
.exchangeStrategies(strategies) .exchangeStrategies(strategies)
.build(); .build();
@@ -65,8 +72,8 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
@Override @Override
public byte[] generateImage(String prompt) { public byte[] generateImage(String prompt) {
if (apiKey == null || apiKey.trim().isEmpty()) { if (projectId == null || projectId.trim().isEmpty()) {
logger.error("API Key is missing!"); logger.error("Project ID is missing! Check application.properties");
return null; return null;
} }
@@ -78,41 +85,63 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
Thread.sleep(rateLimitDelayMs - timeSinceLast); 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 enrichedPrompt = enrichPromptForGemini(prompt);
String endpoint = model + ":predict";
Map<String, Object> requestBody = buildImagenRequestBody(enrichedPrompt); 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() Map<String, Object> response = webClient.post()
.uri(uriBuilder -> uriBuilder .uri(URI.create(endpointUrl))
.path(endpoint) .header("Authorization", "Bearer " + accessToken) // Авторизация через токен
.queryParam("key", apiKey)
.build())
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody) .bodyValue(requestBody)
.retrieve() .retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {}) .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.retryWhen(createRetrySpecFor429("generateImage")) .retryWhen(createRetrySpecFor429())
.doOnError(e -> logger.error("API Error: {}", e.getMessage())) .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)); .block(Duration.ofMillis(timeoutMs));
lastRequestTimestamp = System.currentTimeMillis(); lastRequestTimestamp = System.currentTimeMillis();
return extractImageFromImagenResponse(response); return extractImageFromImagenResponse(response);
} catch (Exception e) { } 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; return null;
} finally { } finally {
semaphore.release(); 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) { private Map<String, Object> buildImagenRequestBody(String prompt) {
Map<String, Object> body = new HashMap<>(); Map<String, Object> body = new HashMap<>();
@@ -120,13 +149,11 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
Map<String, Object> instance = new HashMap<>(); Map<String, Object> instance = new HashMap<>();
instance.put("prompt", prompt); instance.put("prompt", prompt);
instances.add(instance); instances.add(instance);
body.put("instances", instances); body.put("instances", instances);
Map<String, Object> parameters = new HashMap<>(); Map<String, Object> parameters = new HashMap<>();
parameters.put("sampleCount", 1); // Генерируем 1 картинку parameters.put("sampleCount", 1); // Количество картинок
parameters.put("aspectRatio", "1:1"); // Можно вынести в настройки parameters.put("aspectRatio", "1:1"); // Пропорции
// parameters.put("personGeneration", "allow_adult"); // Если нужно (зависит от прав API ключа)
body.put("parameters", parameters); body.put("parameters", parameters);
return body; return body;
@@ -141,9 +168,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
if (predictions != null && !predictions.isEmpty()) { if (predictions != null && !predictions.isEmpty()) {
Map<String, Object> firstPrediction = predictions.get(0); Map<String, Object> firstPrediction = predictions.get(0);
// Imagen на Vertex AI обычно отдает поле "bytesBase64Encoded"
String base64Image = (String) firstPrediction.get("bytesBase64Encoded"); String base64Image = (String) firstPrediction.get("bytesBase64Encoded");
if (base64Image == null) { if (base64Image == null) {
// Резервный вариант, если формат API изменится
base64Image = (String) firstPrediction.get("b64"); base64Image = (String) firstPrediction.get("b64");
} }
@@ -151,14 +181,14 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
return java.util.Base64.getDecoder().decode(base64Image); 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) { } catch (Exception e) {
logger.error("Parsing error: {}", e.getMessage()); logger.error("Parsing error: {}", e.getMessage());
} }
return null; 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)) return reactor.util.retry.Retry.backoff(maxRetryAttempts, Duration.ofSeconds(5))
.maxBackoff(Duration.ofSeconds(30)) .maxBackoff(Duration.ofSeconds(30))
.filter(t -> t instanceof WebClientResponseException && .filter(t -> t instanceof WebClientResponseException &&
@@ -166,6 +196,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
} }
private String enrichPromptForGemini(String original) { private String enrichPromptForGemini(String original) {
return "High quality, photorealistic image of: " + original; return "high quality, photorealistic image of: " + original;
} }
} }
+5 -4
View File
@@ -94,13 +94,14 @@ openai.image.style=natural
# Set to 'nano-banana' to use Google Gemini Nano Banana for image generation # Set to 'nano-banana' to use Google Gemini Nano Banana for image generation
image.generation.provider=nano-banana image.generation.provider=nano-banana
# Google Gemini Nano Banana Image Generation Configuration google.cloud.project-id=onyx-yeti-456518-d4
# Get API key from: https://aistudio.google.com/ google.cloud.location=us-central1
google.gemini.api.key=AIzaSyDMhORdwYaqlXUDP--mmxXUWozFxMAp2hY
google.gemini.image.model=imagen-3.0-generate-001 google.gemini.image.model=imagen-3.0-generate-001
google.gemini.timeoutMs=120000 google.gemini.timeoutMs=120000
google.gemini.retry.maxAttempts=3 google.gemini.retry.maxAttempts=3
google.gemini.retry.initialDelayMs=10000 \google.gemini.retry.initialDelayMs=10000
google.gemini.retry.maxDelayMs=120000 google.gemini.retry.maxDelayMs=120000
google.gemini.retry.multiplier=2.0 google.gemini.retry.multiplier=2.0
google.gemini.rateLimit.delayMs=12000 google.gemini.rateLimit.delayMs=12000