From 679fe31c694c1d58f9ff2c33ff30c25a262eb284 Mon Sep 17 00:00:00 2001 From: arys Date: Sun, 1 Mar 2026 14:01:54 +0500 Subject: [PATCH] fix --- .../MarketingAnalysisV3Controller.java | 43 ++++++------ .../service/MarketingStrategyV3Service.java | 67 +++++++++++-------- .../NanoBananaImageGenerationService.java | 52 +++++++------- 3 files changed, 88 insertions(+), 74 deletions(-) diff --git a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java index 2ccaa5e..c6cf523 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java @@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -258,7 +259,7 @@ public class MarketingAnalysisV3Controller { @RequestHeader(value = "Authorization", required = false) String authHeader, @PathVariable String analysisId, @RequestPart(value = "request", required = false) MarketingStrategyRequest request, - @RequestPart(value = "logo", required = false) MultipartFile logoFile + @RequestPart(value = "references", required = false) List referenceFiles ) { String userId = extractUserIdFromHeader(authHeader); if (userId == null) return unauthorizedResponse(); @@ -268,19 +269,24 @@ public class MarketingAnalysisV3Controller { if (analysisOpt.isEmpty()) return notFoundResponse("Анализ не найден"); if (!analysisOpt.get().getUserId().equals(userId)) return forbiddenResponse(); - String logoFilename = null; - if (logoFile != null && !logoFile.isEmpty()) { - String originalExt = logoFile.getOriginalFilename() != null && logoFile.getOriginalFilename().contains(".") ? - logoFile.getOriginalFilename().substring(logoFile.getOriginalFilename().lastIndexOf(".")) : ".png"; - logoFilename = "logo_" + UUID.randomUUID() + originalExt; - minIOService.uploadFile(logoFilename, logoFile.getBytes(), logoFile.getContentType()); + List savedReferenceFilenames = new ArrayList<>(); + if (referenceFiles != null && !referenceFiles.isEmpty()) { + for (MultipartFile file : referenceFiles) { + if (!file.isEmpty()) { + String originalExt = file.getOriginalFilename() != null && file.getOriginalFilename().contains(".") ? + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")) : ".png"; + String filename = "ref_" + UUID.randomUUID() + originalExt; + minIOService.uploadFile(filename, file.getBytes(), file.getContentType()); + savedReferenceFilenames.add(filename); + } + } } if (request == null) { request = new MarketingStrategyRequest(); } - MarketingStrategy strategy = strategyService.generateStrategy(analysisId, request, userId, logoFilename); + MarketingStrategy strategy = strategyService.generateStrategy(analysisId, request, userId, savedReferenceFilenames); Map responseData = Map.of( "strategyId", strategy.getId(), @@ -478,17 +484,16 @@ public class MarketingAnalysisV3Controller { MarketingStrategy.PostCalendarItem updatedItem = strategyService.regeneratePostImage(strategyId, postIndex); if (updatedItem == null) return notFoundResponse("Пост не найден"); - Map responseData = Map.of( - "strategyId", strategyId, - "postIndex", postIndex, - "imageUrl", updatedItem.getImageUrl() != null ? updatedItem.getImageUrl() : "", - "imageFilename", updatedItem.getImageFilename() != null ? updatedItem.getImageFilename() : "", - "videoUrl", updatedItem.getVideoUrl() != null ? updatedItem.getVideoUrl() : "", - "videoFilename", updatedItem.getVideoFilename() != null ? updatedItem.getVideoFilename() : "", - "theme", updatedItem.getTheme() != null ? updatedItem.getTheme() : "", - "platform", updatedItem.getPlatform() != null ? updatedItem.getPlatform() : "", - "publishDate", updatedItem.getPublishDate() - ); + Map responseData = new HashMap<>(); + responseData.put("strategyId", strategyId); + responseData.put("postIndex", postIndex); + responseData.put("imageUrl", updatedItem.getImageUrl() != null ? updatedItem.getImageUrl() : ""); + responseData.put("imageFilename", updatedItem.getImageFilename() != null ? updatedItem.getImageFilename() : ""); + responseData.put("videoUrl", updatedItem.getVideoUrl() != null ? updatedItem.getVideoUrl() : ""); + responseData.put("videoFilename", updatedItem.getVideoFilename() != null ? updatedItem.getVideoFilename() : ""); + responseData.put("theme", updatedItem.getTheme() != null ? updatedItem.getTheme() : ""); + responseData.put("platform", updatedItem.getPlatform() != null ? updatedItem.getPlatform() : ""); + responseData.put("publishDate", updatedItem.getPublishDate()); return ResponseEntity.ok(ApiResponse.success("Изображение для поста успешно регенерировано", responseData)); } catch (Exception e) { diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java index dd6e471..14d3e75 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java @@ -57,7 +57,7 @@ public class MarketingStrategyV3Service { return repository.findByUserIdOrderByCreatedAtDesc(userId); } - public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, String uploadedLogoFilename) { + public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, List referenceFilenames) { MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId) .orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found")); @@ -68,8 +68,8 @@ public class MarketingStrategyV3Service { strategy.setStatus("queued"); Map initialData = new HashMap<>(); - if (uploadedLogoFilename != null && !uploadedLogoFilename.isEmpty()) { - initialData.put("clientLogoFilename", uploadedLogoFilename); + if (referenceFilenames != null && !referenceFilenames.isEmpty()) { + initialData.put("referenceFilenames", referenceFilenames); } strategy.setStrategyData(initialData); @@ -125,7 +125,6 @@ public class MarketingStrategyV3Service { } } - // ИДЕАЛЬНЫЙ СБОРЩИК ОТВЕТА (Интеграция с Автопостингом) public Map getStrategyResult(String strategyId) { Optional optStrategy = repository.findById(strategyId); if (optStrategy.isEmpty()) { @@ -190,6 +189,7 @@ public class MarketingStrategyV3Service { return response; } + @SuppressWarnings("unchecked") public MarketingStrategy.PostCalendarItem regeneratePostImage(String strategyId, int postIndex) { Optional optStrategy = repository.findById(strategyId); if (optStrategy.isEmpty()) { @@ -214,19 +214,22 @@ public class MarketingStrategyV3Service { String businessContext = getBusinessContext(analysis); String brandName = analysis.getRequestData().getProductName(); - byte[] clientLogoBytes = null; - if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("clientLogoFilename")) { - String logoFilename = (String) strategy.getStrategyData().get("clientLogoFilename"); - try { - InputStream logoStream = minIOService.downloadFile(logoFilename); - clientLogoBytes = logoStream.readAllBytes(); - logoStream.close(); - } catch (Exception e) { - log.error("Failed to download logo for regeneration: {}", e.getMessage()); + byte[] clientRefBytes = null; + if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("referenceFilenames")) { + List refFiles = (List) strategy.getStrategyData().get("referenceFilenames"); + if (refFiles != null && !refFiles.isEmpty()) { + String randomRef = refFiles.get(new Random().nextInt(refFiles.size())); + try { + InputStream logoStream = minIOService.downloadFile(randomRef); + clientRefBytes = logoStream.readAllBytes(); + logoStream.close(); + } catch (Exception e) { + log.error("Failed to download ref file for regeneration: {}", e.getMessage()); + } } } - doGenerateImage(item, businessContext, brandName, clientLogoBytes); + doGenerateImage(item, businessContext, brandName, clientRefBytes); repository.save(strategy); return item; } @@ -334,25 +337,33 @@ public class MarketingStrategyV3Service { strategy.setPostCalendar(postCalendar); } + @SuppressWarnings("unchecked") private void generateMediaAssets(MarketingStrategy strategy, MarketingAnalysisV3Document analysis) { String businessContext = getBusinessContext(analysis); String brandName = analysis.getRequestData().getProductName(); - byte[] clientLogoBytes = null; - if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("clientLogoFilename")) { - String logoFilename = (String) strategy.getStrategyData().get("clientLogoFilename"); - try { - InputStream logoStream = minIOService.downloadFile(logoFilename); - clientLogoBytes = logoStream.readAllBytes(); - logoStream.close(); - } catch (Exception e) { - log.error("Failed to download logo: {}", e.getMessage()); - } + List refFiles = new ArrayList<>(); + if (strategy.getStrategyData() != null && strategy.getStrategyData().containsKey("referenceFilenames")) { + refFiles = (List) strategy.getStrategyData().get("referenceFilenames"); } + Random random = new Random(); + for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) { String contentType = item.getContentType() != null ? item.getContentType().toLowerCase() : "фото"; + byte[] currentRefBytes = null; + if (!refFiles.isEmpty()) { + String selectedRef = refFiles.get(random.nextInt(refFiles.size())); + try { + InputStream logoStream = minIOService.downloadFile(selectedRef); + currentRefBytes = logoStream.readAllBytes(); + logoStream.close(); + } catch (Exception e) { + log.error("Failed to download ref file {}: {}", selectedRef, e.getMessage()); + } + } + try { if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) { String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k."; @@ -371,10 +382,10 @@ public class MarketingStrategyV3Service { item.setVideoFilename(filename); } else { item.setContentType("фото"); - doGenerateImage(item, businessContext, brandName, clientLogoBytes); + doGenerateImage(item, businessContext, brandName, currentRefBytes); } } else { - doGenerateImage(item, businessContext, brandName, clientLogoBytes); + doGenerateImage(item, businessContext, brandName, currentRefBytes); } if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs); @@ -385,10 +396,10 @@ public class MarketingStrategyV3Service { } } - private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientLogoBytes) { + private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientRefBytes) { String imagePrompt = buildImagePrompt(item, businessContext, brandName); try { - byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientLogoBytes); + byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientRefBytes); if (imageBytes != null && imageBytes.length > 0) { String filename = "image_" + System.currentTimeMillis() + "_" + item.hashCode() + ".png"; minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE); diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index e471fa5..e8c7567 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -67,13 +67,11 @@ public class NanoBananaImageGenerationService implements ImageGenerationService .build(); } - // Стандартный метод (если логотипа нет) @Override public byte[] generateImage(String prompt) { return generateImageWithReference(prompt, (byte[]) null); } - // НОВЫЙ МЕТОД: Принимает MultipartFile от контроллера public byte[] generateImage(String prompt, MultipartFile referenceLogo) { if (referenceLogo == null || referenceLogo.isEmpty()) { return generateImage(prompt); @@ -82,11 +80,10 @@ public class NanoBananaImageGenerationService implements ImageGenerationService return generateImageWithReference(prompt, referenceLogo.getBytes()); } catch (IOException e) { logger.error("Ошибка при чтении MultipartFile логотипа: {}", e.getMessage()); - return generateImage(prompt); // Fallback на обычную генерацию, если файл битый + return generateImage(prompt); } } - // Основная логика генерации с поддержкой референсного изображения (логотипа) public byte[] generateImageWithReference(String prompt, byte[] referenceImageBytes) { if (projectId == null || projectId.trim().isEmpty()) { logger.error("Project ID is missing! Check application.properties"); @@ -108,17 +105,11 @@ public class NanoBananaImageGenerationService implements ImageGenerationService String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); - String finalPrompt = prompt; - if (prompt != null && prompt.length() < 50) { - finalPrompt = "High quality, photorealistic image of: " + prompt; - } + // Очищаем промпт от прямых указаний "написать текст" + String finalPrompt = sanitizePromptForText(prompt); - // Передаем байты картинки в сборщик JSON Map requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes); - logger.info("Sending request to Vertex AI. Project: {}, Model: {}. With Logo: {}", - projectId, model, (referenceImageBytes != null)); - Map response = webClient.post() .uri(URI.create(endpointUrl)) .header("Authorization", "Bearer " + accessToken) @@ -127,12 +118,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) .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(); @@ -150,7 +135,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService try { ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH); if (!resource.exists()) { - logger.error("Google Credentials file not found at classpath: {}", CREDENTIALS_FILE_PATH); return null; } @@ -161,12 +145,21 @@ public class NanoBananaImageGenerationService implements ImageGenerationService return credentials.getAccessToken().getTokenValue(); } } catch (IOException e) { - logger.error("Error loading Google Credentials from resources", e); return null; } } - // ОБНОВЛЕННЫЙ БИЛДЕР: Вшивает логотип в JSON запрос + // Умная очистка промпта: не позволяем ИИ пытаться писать текст буквами + 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; + } + private Map buildImagenRequestBody(String prompt, byte[] referenceImageBytes) { Map body = new HashMap<>(); List> instances = new ArrayList<>(); @@ -174,7 +167,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService instance.put("prompt", prompt); - // Если передали логотип — добавляем его как SUBJECT REFERENCE if (referenceImageBytes != null && referenceImageBytes.length > 0) { String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes); @@ -183,13 +175,18 @@ public class NanoBananaImageGenerationService implements ImageGenerationService imageBytesMap.put("bytesBase64Encoded", base64Image); referenceImageParams.put("referenceImage", imageBytesMap); - // SUBJECT - заставляет ИИ интегрировать этот объект в сцену - referenceImageParams.put("referenceType", "SUBJECT"); + + // STYLE заставляет ИИ использовать загруженное фото как логотип/паттерн, + // а не пытаться впихнуть 3D-модель объекта. Для логотипов это работает лучше, чем SUBJECT. + referenceImageParams.put("referenceType", "STYLE"); 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."); } instances.add(instance); @@ -197,10 +194,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService Map parameters = new HashMap<>(); parameters.put("sampleCount", 1); - parameters.put("aspectRatio", "1:1"); // Можно вынести в настройки, если нужны сторисы 9:16 + 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, signature, low quality, blurry, distorted, unrealistic"); + + // Жесткий негативный промпт против генерации кривого текста + parameters.put("negativePrompt", "text, typography, letters, words, writing, signature, watermark, nsfw, nudity, violence, deformed, ugly, blurry, distorted"); body.put("parameters", parameters); return body; @@ -221,7 +220,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService return java.util.Base64.getDecoder().decode(base64Image); } } - logger.warn("No image data found in Vertex AI response."); } catch (Exception e) { logger.error("Parsing error: {}", e.getMessage()); }