com
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user