From e7f5b63635400a8de007d1e0d9770472fc3866a5 Mon Sep 17 00:00:00 2001 From: arys Date: Wed, 7 Jan 2026 22:03:44 +0500 Subject: [PATCH 1/2] com --- .../NanoBananaImageGenerationService.java | 66 +++++++++---------- src/main/resources/application.properties | 2 +- src/main/resources/keys/google-key.json | 13 ++++ 3 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 src/main/resources/keys/google-key.json diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index 97d130c..347ab2d 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.ExchangeStrategies; @@ -16,20 +17,16 @@ import reactor.netty.http.client.HttpClient; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.Semaphore; @Service public class NanoBananaImageGenerationService implements ImageGenerationService { private static final Logger logger = LoggerFactory.getLogger(NanoBananaImageGenerationService.class); - - // Шаблон 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); @@ -54,14 +51,15 @@ public class NanoBananaImageGenerationService implements ImageGenerationService @Value("${google.gemini.rateLimit.delayMs:12000}") private long rateLimitDelayMs; + private static final String CREDENTIALS_FILE_PATH = "keys/google-key.json"; + public NanoBananaImageGenerationService() { HttpClient httpClient = HttpClient.create() .resolver(DefaultAddressResolverGroup.INSTANCE) .responseTimeout(Duration.ofMillis(120000)); - // Увеличиваем буфер памяти для приема больших картинок (Base64) ExchangeStrategies strategies = ExchangeStrategies.builder() - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(20 * 1024 * 1024)) // 20MB + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(20 * 1024 * 1024)) .build(); this.webClient = WebClient.builder() @@ -85,25 +83,21 @@ public class NanoBananaImageGenerationService implements ImageGenerationService Thread.sleep(rateLimitDelayMs - timeSinceLast); } - String accessToken = getAccessToken(); + // Получаем токен из файла ресурсов + String accessToken = getAccessTokenFromResources(); 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); Map requestBody = buildImagenRequestBody(enrichedPrompt); logger.info("Sending request to Vertex AI. Project: {}, Model: {}", projectId, model); - // 3. Выполняем POST запрос Map response = webClient.post() .uri(URI.create(endpointUrl)) - .header("Authorization", "Bearer " + accessToken) // Авторизация через токен + .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() @@ -128,23 +122,32 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } } - - private String getAccessToken() { + /** + * Читает ключ прямо из ресурсов проекта (src/main/resources/keys/google-key.json) + */ + private String getAccessTokenFromResources() { try { - GoogleCredentials credentials = GoogleCredentials.getApplicationDefault() - .createScoped("https://www.googleapis.com/auth/cloud-platform"); - credentials.refreshIfExpired(); - AccessToken token = credentials.getAccessToken(); - return token.getTokenValue(); + // Загружаем файл из classpath + ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH); + if (!resource.exists()) { + logger.error("Google Credentials file not found at classpath: {}", CREDENTIALS_FILE_PATH); + return null; + } + + try (InputStream is = resource.getInputStream()) { + GoogleCredentials credentials = GoogleCredentials.fromStream(is) + .createScoped("https://www.googleapis.com/auth/cloud-platform"); + credentials.refreshIfExpired(); + return credentials.getAccessToken().getTokenValue(); + } } catch (IOException e) { - logger.error("Error obtaining Google Credentials", e); + logger.error("Error loading Google Credentials from resources", e); return null; } } private Map buildImagenRequestBody(String prompt) { Map body = new HashMap<>(); - List> instances = new ArrayList<>(); Map instance = new HashMap<>(); instance.put("prompt", prompt); @@ -152,8 +155,8 @@ public class NanoBananaImageGenerationService implements ImageGenerationService body.put("instances", instances); Map parameters = new HashMap<>(); - parameters.put("sampleCount", 1); // Количество картинок - parameters.put("aspectRatio", "1:1"); // Пропорции + parameters.put("sampleCount", 1); + parameters.put("aspectRatio", "1:1"); body.put("parameters", parameters); return body; @@ -162,26 +165,19 @@ public class NanoBananaImageGenerationService implements ImageGenerationService @SuppressWarnings("unchecked") private byte[] extractImageFromImagenResponse(Map response) { if (response == null) return null; - try { List> predictions = (List>) response.get("predictions"); - 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"); } - if (base64Image != null) { return java.util.Base64.getDecoder().decode(base64Image); } } - logger.warn("No image data found in Vertex AI response. Response: {}", response); + logger.warn("No image data found in Vertex AI response."); } catch (Exception e) { logger.error("Parsing error: {}", e.getMessage()); } @@ -196,6 +192,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 3d1cab1..3a9c7e7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -94,7 +94,7 @@ openai.image.style=natural # Set to 'nano-banana' to use Google Gemini Nano Banana for image generation image.generation.provider=nano-banana -google.cloud.project-id=onyx-yeti-456518-d4 +google.cloud.project-id=data-totality-483616-t5 google.cloud.location=us-central1 google.gemini.image.model=imagen-3.0-generate-001 diff --git a/src/main/resources/keys/google-key.json b/src/main/resources/keys/google-key.json new file mode 100644 index 0000000..df9ff33 --- /dev/null +++ b/src/main/resources/keys/google-key.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "data-totality-483616-t5", + "private_key_id": "e9d6df08cfaf391f2b45ccf0473d22221a14fe1e", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkA41lMjU7GSLj\nS5CeXJjT3gsKTp2/Y9adieLrjIXA8GY3Dhm+dwj1+hVF1fKB1qYI072LNku9jpuA\nGbKtDAIDXfTr0NKF097z51+X7Sprnq3lamqzAXnH8PEAdv5A97GtNBDBB6E9S40q\n6uEBfIjeaVENcrt8bnS/jgvtQeDFj/0RoFVOUfapCgFW2zkvqU549z0kZumF9zNF\nNXkKGOelmWm1430QyCWhw4P5SIxfCehz0pD17NSMlUZQvbav/ZZ286SnDzloRT+q\njXyRIXnMMuTnKWov7zqKflmfg8omxkwf/HqGIu2c0+hgmcyQhy8oXj/X9ocXLNsA\n+IclAskFAgMBAAECggEAD+ZHl5GhnGbdksg5WZ4OCK05R8NYQnyktPqayg0szYfg\neMzVTQ6lX+cMQeFcKXRhUxtnqq5Vnad3Rpih0Wmya5jOTJVJRe+Z6O4Qk8ifeKBW\nKOvDMogy7IwxvhhK9zRWf+e7X2MPIUMziP9iVu5iJvbHBIqPR0rpPBqX6GT0VmJs\n6CG1C1FtL3QFvxiZSiWwIzGb0ozZKIHBhqUxapQcUlTSqsSNguJEGeHUXkD3Eg1R\n3gQJs4JKCiQnsCeWcpEcoo/GaP+feTjXDJzPlx6EDzpaFAeyUXPWNm8s8VBHj/dM\nvD9FSL2/YiouublF7TW+cvCCfiYALLJx41hOeeEZTwKBgQDhtbdY1FQwh6T8ciF2\nCH+kWfIUIIiNUwlyllc05Vx6nC9oSuChDOWFMqe2TOpcibAvPCWgLN4lHjxsd0zj\nInTomGFg8mPu7l9g0AUCt6R2DoWWnS2omtyRTCIiq0Ms8WHUwil3U57IS7+2MgK7\nlKwBQtrBQoWZPkgwiuxypLf/uwKBgQC6BkP3B2u2LbP5v0VlNYI1Lvr96Bk6sWCh\ny1kOO0DI/6ubC5WFgTDtTBakbZvRdk5X7OmJCaMRtNCgrAK1twq8dLT5EZFNRrQ/\n+Q/mGsMBnZlf15hqUVnjmYuXy+o9hnU1F84TOjz0AYwvZFmoWicYWf6NDoHfnXyw\nMQOlQMHuPwKBgQC4wBFPuJ0hDCbULGePQe2ZDbbFGswrQejdzombVpKYGfgEGiY8\nLV5B5PC6m8LJ9Dour8Dsl01OfK1g61YYJUAkk3ZB8KY0ERIpIJy5WLBZohFe81FI\nTVp15Cec0SqvQvWdPUDL17qEoNxPuifaexs//QUqY9UPVpeohE5+mK2zuQKBgD8S\nGMEMlwxxem/W1eR2u+rkPt0PcF5h/W1d0Jfupwx/AuPSjaNyJfCOt66QKyBdiDpv\nbM61ocBDGekf6NtLcCxtNV6tAiK9MifufeF6eVt236WLJ3YElwF/8cBJtuWdTOfv\nZkNGn5vrWmhHEc7uogqzmIaiwI3ci4YBWEcTKa1pAoGAQAGOKD6/9m15rGoypQK0\ndM7RHoFiPAga0l6RaRmjInhkPItood84nWvSGwmfDMovvLuMXT4/kqhgoAJBCTp4\nZMWlEnk5XgU3XVATV9fWogSEdPiXq+mwikl2A7xF/PsS79ekc6wVAFS6aog08GH+\ndwLlquOtv2Dd9qW6d6Pe+4I=\n-----END PRIVATE KEY-----\n", + "client_email": "vertex-ai-user@data-totality-483616-t5.iam.gserviceaccount.com", + "client_id": "116751741947593459625", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-user%40data-totality-483616-t5.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} From daa8b6118d86e9c77934c49595fbf6827fc3629e Mon Sep 17 00:00:00 2001 From: arys Date: Wed, 7 Jan 2026 22:17:04 +0500 Subject: [PATCH 2/2] com --- .../service/NanoBananaImageGenerationService.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index 347ab2d..b703356 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -1,6 +1,5 @@ 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; @@ -28,6 +27,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService private static final Logger logger = LoggerFactory.getLogger(NanoBananaImageGenerationService.class); private static final String VERTEX_API_TEMPLATE = "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predict"; + private static final String CREDENTIALS_FILE_PATH = "keys/google-key.json"; private final Semaphore semaphore = new Semaphore(1); private volatile long lastRequestTimestamp = 0; @@ -51,8 +51,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService @Value("${google.gemini.rateLimit.delayMs:12000}") private long rateLimitDelayMs; - private static final String CREDENTIALS_FILE_PATH = "keys/google-key.json"; - public NanoBananaImageGenerationService() { HttpClient httpClient = HttpClient.create() .resolver(DefaultAddressResolverGroup.INSTANCE) @@ -83,7 +81,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService Thread.sleep(rateLimitDelayMs - timeSinceLast); } - // Получаем токен из файла ресурсов String accessToken = getAccessTokenFromResources(); if (accessToken == null) { return null; @@ -122,12 +119,8 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } } - /** - * Читает ключ прямо из ресурсов проекта (src/main/resources/keys/google-key.json) - */ private String getAccessTokenFromResources() { try { - // Загружаем файл из classpath ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH); if (!resource.exists()) { logger.error("Google Credentials file not found at classpath: {}", CREDENTIALS_FILE_PATH); @@ -157,6 +150,9 @@ public class NanoBananaImageGenerationService implements ImageGenerationService Map parameters = new HashMap<>(); parameters.put("sampleCount", 1); parameters.put("aspectRatio", "1:1"); + parameters.put("safetyFilterLevel", "block_some"); + parameters.put("personGeneration", "allow_adult"); + parameters.put("negativePrompt", "nsfw, nudity, sexual content, lgbt symbols, rainbow flags, provocative clothing, violence, gore, blood, deformed, ugly, watermark, text, signature"); body.put("parameters", parameters); return body; @@ -192,6 +188,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } private String enrichPromptForGemini(String original) { - return "High quality, photorealistic image of: " + original; + return "Professional, high quality, photorealistic image of: " + original + + ". Style: neutral, business-appropriate, culturally respectful."; } } \ No newline at end of file