From 19e008b7ffe7828c73367d7898c2c7cb0dac42bf Mon Sep 17 00:00:00 2001 From: arys Date: Sun, 1 Mar 2026 14:23:25 +0500 Subject: [PATCH] fix --- .../MarketingAnalysisV3Controller.java | 37 ++++++ .../service/MarketingStrategyV3Service.java | 110 ++++++++++++++---- .../NanoBananaImageGenerationService.java | 25 ++-- 3 files changed, 130 insertions(+), 42 deletions(-) diff --git a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java index c6cf523..69675d9 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java @@ -501,6 +501,43 @@ public class MarketingAnalysisV3Controller { } } + // НОВЫЙ ЭНДПОИНТ ДЛЯ РЕГЕНЕРАЦИИ ВИДЕО + @PostMapping("/strategy/{strategyId}/post/{postIndex}/regenerate-video") + public ResponseEntity regeneratePostVideo( + @RequestHeader(value = "Authorization", required = false) String authHeader, + @PathVariable String strategyId, + @PathVariable int postIndex + ) { + String userId = extractUserIdFromHeader(authHeader); + if (userId == null) return unauthorizedResponse(); + + try { + Optional optStrategy = strategyService.getStrategyById(strategyId); + if (optStrategy.isEmpty()) return notFoundResponse("Стратегия не найдена"); + + MarketingStrategy strategy = optStrategy.get(); + if (!strategy.getUserId().equals(userId)) return forbiddenResponse(); + + MarketingStrategy.PostCalendarItem updatedItem = strategyService.regeneratePostVideo(strategyId, postIndex); + if (updatedItem == null) return notFoundResponse("Пост не найден"); + + 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) { + return internalErrorResponse(e); + } + } + private StrategyHistoryResponse convertToStrategyHistoryResponse(MarketingStrategy strategy) { StrategyHistoryResponse response = new StrategyHistoryResponse(); response.setStrategyId(strategy.getId()); diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java index 14d3e75..df33879 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java @@ -42,7 +42,7 @@ public class MarketingStrategyV3Service { @Value("${openai.model.name.text:gpt-4o}") private String highIntelligenceModel; - @Value("${image.generation.delayBetweenRequestsMs:3000}") + @Value("${image.generation.delayBetweenRequestsMs:5000}") // Увеличили задержку до 5 сек private long delayBetweenRequestsMs; public Optional getStrategyById(String id) { @@ -205,6 +205,9 @@ public class MarketingStrategyV3Service { MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); + // Убеждаемся, что тип контента фото, если регенерируем фото + item.setContentType("фото"); + Optional analysisOpt = analysisRepository.findById(strategy.getAnalysisId()); if (analysisOpt.isEmpty()) { return null; @@ -234,6 +237,40 @@ public class MarketingStrategyV3Service { return item; } + // НОВЫЙ МЕТОД ДЛЯ РЕГЕНЕРАЦИИ ВИДЕО + public MarketingStrategy.PostCalendarItem regeneratePostVideo(String strategyId, int postIndex) { + Optional optStrategy = repository.findById(strategyId); + if (optStrategy.isEmpty()) { + return null; + } + + MarketingStrategy strategy = optStrategy.get(); + List postCalendar = strategy.getPostCalendar(); + + if (postCalendar == null || postIndex < 0 || postIndex >= postCalendar.size()) { + return null; + } + + MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); + + // Меняем тип контента на видео + item.setContentType("видео"); + + Optional analysisOpt = analysisRepository.findById(strategy.getAnalysisId()); + if (analysisOpt.isEmpty()) { + return null; + } + + MarketingAnalysisV3Document analysis = analysisOpt.get(); + String businessContext = getBusinessContext(analysis); + + doGenerateVideo(item, businessContext); + + // Если видео не сгенерировалось (фоллбек на фото), сохраняем изменения + repository.save(strategy); + return item; + } + public StrategyModel calculateBestScoringModel(kz.konturai.parser.dto.MarketingAnalysisV3Request req) { int entry = 0, authority = 0, trust = 0, conversion = 0; @@ -366,24 +403,7 @@ public class MarketingStrategyV3Service { 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."; - - byte[] videoBytes = null; - try { - videoBytes = geminiVideoService.generateVideo(videoPrompt); - } catch (Exception e) { - log.warn("Video service failed: {}", e.getMessage()); - } - - if (videoBytes != null && videoBytes.length > 0) { - String filename = "video_" + System.currentTimeMillis() + "_" + item.hashCode() + ".mp4"; - minIOService.uploadFile(filename, videoBytes, "video/mp4"); - item.setVideoUrl(filename); - item.setVideoFilename(filename); - } else { - item.setContentType("фото"); - doGenerateImage(item, businessContext, brandName, currentRefBytes); - } + doGenerateVideoWithFallback(item, businessContext, brandName, currentRefBytes); } else { doGenerateImage(item, businessContext, brandName, currentRefBytes); } @@ -396,6 +416,50 @@ public class MarketingStrategyV3Service { } } + // Выделенный метод для генерации видео с фоллбеком + private void doGenerateVideoWithFallback(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] currentRefBytes) { + String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k."; + + byte[] videoBytes = null; + try { + videoBytes = geminiVideoService.generateVideo(videoPrompt); + } catch (Exception e) { + log.warn("Video service failed: {}", e.getMessage()); + } + + if (videoBytes != null && videoBytes.length > 0) { + String filename = "video_" + System.currentTimeMillis() + "_" + item.hashCode() + ".mp4"; + minIOService.uploadFile(filename, videoBytes, "video/mp4"); + item.setVideoUrl(filename); + item.setVideoFilename(filename); + } else { + log.warn("Fallback: Video generation failed. Switching to Image generation for theme: {}", item.getTheme()); + item.setContentType("фото"); + doGenerateImage(item, businessContext, brandName, currentRefBytes); + } + } + + // Выделенный метод генерации видео БЕЗ фоллбека (для ручной регенерации) + private void doGenerateVideo(MarketingStrategy.PostCalendarItem item, String businessContext) { + String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k."; + try { + byte[] videoBytes = geminiVideoService.generateVideo(videoPrompt); + if (videoBytes != null && videoBytes.length > 0) { + String filename = "video_" + System.currentTimeMillis() + "_" + item.hashCode() + ".mp4"; + minIOService.uploadFile(filename, videoBytes, "video/mp4"); + item.setVideoUrl(filename); + item.setVideoFilename(filename); + // Очищаем данные фото, так как теперь это видео + item.setImageUrl(null); + item.setImageFilename(null); + } else { + log.error("Failed to regenerate video, returned null bytes"); + } + } catch (Exception e) { + log.error("Error regenerating video: {}", e.getMessage()); + } + } + private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientRefBytes) { String imagePrompt = buildImagePrompt(item, businessContext, brandName); try { @@ -405,6 +469,9 @@ public class MarketingStrategyV3Service { minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE); item.setImageUrl(filename); item.setImageFilename(filename); + // Очищаем данные видео, так как теперь это фото + item.setVideoUrl(null); + item.setVideoFilename(null); } } catch (Exception e) { log.error("Image generation failed: {}", e.getMessage()); @@ -435,16 +502,11 @@ public class MarketingStrategyV3Service { prompt.append("Context: ").append(businessContext).append(". "); } - if (brandName != null && !brandName.isEmpty()) { - prompt.append("Subtly and naturally integrate the brand name '").append(brandName).append("' into the physical environment (e.g., on a sign, package, mug, or notebook). "); - } - if (item.getTheme() != null && !item.getTheme().isEmpty()) { prompt.append("Main subject/action: ").append(item.getTheme()).append(". "); } prompt.append("Composition: Product or service is the main focus. People (if any) should look natural. "); - prompt.append("Avoid: abstract concepts, people staring blankly at camera, fake poses, text overlay, blur, distortion."); return prompt.toString(); } diff --git a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java index 33f0838..2ac6dc7 100644 --- a/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/NanoBananaImageGenerationService.java @@ -106,14 +106,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); String finalPrompt = sanitizePromptForText(prompt); - // Первая попытка: с логотипом (если он есть) 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); @@ -165,12 +163,9 @@ public class NanoBananaImageGenerationService implements ImageGenerationService private String sanitizePromptForText(String originalPrompt) { if (originalPrompt == null) return "High quality, photorealistic image"; - 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; + return originalPrompt; // Убрали лишнюю очистку, которая могла ломать логику ИИ } - // ИСПРАВЛЕННЫЙ МЕТОД ФОРМИРОВАНИЯ JSON private Map buildImagenRequestBody(String prompt, byte[] referenceImageBytes) { Map body = new HashMap<>(); List> instances = new ArrayList<>(); @@ -178,7 +173,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService instance.put("prompt", prompt); - // Правильная структура referenceImage для Imagen 3 if (referenceImageBytes != null && referenceImageBytes.length > 0) { String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes); @@ -186,22 +180,17 @@ public class NanoBananaImageGenerationService implements ImageGenerationService Map imageBytesMap = new HashMap<>(); imageBytesMap.put("bytesBase64Encoded", base64Image); - // Желательно указывать MIME тип, чтобы Imagen не путался - imageBytesMap.put("mimeType", "image/png"); + // Убрали mimeType, чтобы избежать 400 Bad Request из-за несоответствия типов referenceImageParams.put("referenceImage", imageBytesMap); - // Для интеграции логотипа/предмета в сцену Google рекомендует использовать "SUBJECT" или не указывать тип вообще - // В v1beta API структура referenceImages немного отличается. - referenceImageParams.put("referenceType", "SUBJECT"); + // Возвращаем STYLE, так как он меньше ломает геометрию + referenceImageParams.put("referenceType", "STYLE"); - // Для Imagen 3 referenceImages передается на уровне instance, а не параметров List> refImagesList = new ArrayList<>(); refImagesList.add(referenceImageParams); instance.put("referenceImages", refImagesList); - - // Если используем SUBJECT, промпт должен четко описывать сцену вокруг этого объекта - instance.put("prompt", prompt + ". The image must feature the exact provided reference object/logo naturally integrated into the scene."); + instance.put("prompt", prompt + ". Use the provided reference image."); } instances.add(instance); @@ -212,8 +201,8 @@ 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"); + // Убрали text из негативного промпта + parameters.put("negativePrompt", "nsfw, nudity, violence, deformed, ugly, blurry, distorted"); body.put("parameters", parameters); return body;