Merge remote-tracking branch 'refs/remotes/origin/analyze-graphics' into analyze-graphics
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user