diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index e8c7567..33f0838 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -79,7 +79,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService try { return generateImageWithReference(prompt, referenceLogo.getBytes()); } catch (IOException e) { - logger.error("Ошибка при чтении MultipartFile логотипа: {}", e.getMessage()); + logger.error("Error reading MultipartFile logo: {}", e.getMessage()); return generateImage(prompt); } } @@ -104,24 +104,26 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); - - // Очищаем промпт от прямых указаний "написать текст" String finalPrompt = sanitizePromptForText(prompt); - Map requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes); - - Map response = webClient.post() - .uri(URI.create(endpointUrl)) - .header("Authorization", "Bearer " + accessToken) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(requestBody) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}) - .retryWhen(createRetrySpecFor429()) - .block(Duration.ofMillis(timeoutMs)); - - lastRequestTimestamp = System.currentTimeMillis(); - return extractImageFromImagenResponse(response); + // Первая попытка: с логотипом (если он есть) + try { + Map requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes); + Map response = executeVertexRequest(endpointUrl, accessToken, requestBody); + lastRequestTimestamp = System.currentTimeMillis(); + return extractImageFromImagenResponse(response); + } catch (WebClientResponseException.BadRequest badRequestEx) { + // Если API вернул 400 Bad Request из-за кривого референса, пробуем сгенерировать БЕЗ него + if (referenceImageBytes != null && referenceImageBytes.length > 0) { + logger.warn("Vertex AI rejected the request with reference image (400 Bad Request). Retrying WITHOUT reference image..."); + Map fallbackBody = buildImagenRequestBody(finalPrompt, null); + Map fallbackResponse = executeVertexRequest(endpointUrl, accessToken, fallbackBody); + lastRequestTimestamp = System.currentTimeMillis(); + return extractImageFromImagenResponse(fallbackResponse); + } else { + throw badRequestEx; + } + } } catch (Exception e) { logger.error("Fatal error in NanoBanana (Vertex AI): {}", e.getMessage(), e); @@ -131,6 +133,18 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } } + private Map executeVertexRequest(String endpointUrl, String accessToken, Map requestBody) { + return webClient.post() + .uri(URI.create(endpointUrl)) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .retryWhen(createRetrySpecFor429()) + .block(Duration.ofMillis(timeoutMs)); + } + private String getAccessTokenFromResources() { try { ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH); @@ -149,17 +163,14 @@ public class NanoBananaImageGenerationService implements ImageGenerationService } } - // Умная очистка промпта: не позволяем ИИ пытаться писать текст буквами private String sanitizePromptForText(String originalPrompt) { if (originalPrompt == null) return "High quality, photorealistic image"; - - // Убираем фразы типа "integrate the brand name 'XXX'" String sanitized = originalPrompt.replaceAll("(?i)integrate the brand name '[^']+'", "integrate the provided logo image"); sanitized = sanitized.replaceAll("(?i)with the text '[^']+'", "with the provided logo"); - return sanitized; } + // ИСПРАВЛЕННЫЙ МЕТОД ФОРМИРОВАНИЯ JSON private Map buildImagenRequestBody(String prompt, byte[] referenceImageBytes) { Map body = new HashMap<>(); List> instances = new ArrayList<>(); @@ -167,26 +178,30 @@ public class NanoBananaImageGenerationService implements ImageGenerationService instance.put("prompt", prompt); + // Правильная структура referenceImage для Imagen 3 if (referenceImageBytes != null && referenceImageBytes.length > 0) { String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes); Map referenceImageParams = new HashMap<>(); + Map imageBytesMap = new HashMap<>(); - imageBytesMap.put("bytesBase64Encoded", base64Image); + // Желательно указывать MIME тип, чтобы Imagen не путался + imageBytesMap.put("mimeType", "image/png"); + referenceImageParams.put("referenceImage", imageBytesMap); + // Для интеграции логотипа/предмета в сцену Google рекомендует использовать "SUBJECT" или не указывать тип вообще + // В v1beta API структура referenceImages немного отличается. + referenceImageParams.put("referenceType", "SUBJECT"); - // STYLE заставляет ИИ использовать загруженное фото как логотип/паттерн, - // а не пытаться впихнуть 3D-модель объекта. Для логотипов это работает лучше, чем SUBJECT. - referenceImageParams.put("referenceType", "STYLE"); - + // Для Imagen 3 referenceImages передается на уровне instance, а не параметров List> refImagesList = new ArrayList<>(); refImagesList.add(referenceImageParams); instance.put("referenceImages", refImagesList); - // Если есть референс, усиливаем промпт, чтобы он наклеил его, а не писал текст - instance.put("prompt", prompt + ". Use the provided reference image as a logo or decal on the main object. Do NOT generate any custom text or typography."); + // Если используем SUBJECT, промпт должен четко описывать сцену вокруг этого объекта + instance.put("prompt", prompt + ". The image must feature the exact provided reference object/logo naturally integrated into the scene."); } instances.add(instance); @@ -197,8 +212,7 @@ public class NanoBananaImageGenerationService implements ImageGenerationService parameters.put("aspectRatio", "1:1"); parameters.put("safetyFilterLevel", "block_some"); parameters.put("personGeneration", "allow_adult"); - - // Жесткий негативный промпт против генерации кривого текста + // Жесткий негативный промпт parameters.put("negativePrompt", "text, typography, letters, words, writing, signature, watermark, nsfw, nudity, violence, deformed, ugly, blurry, distorted"); body.put("parameters", parameters);