This commit is contained in:
arys
2026-03-01 14:23:25 +05:00
parent ddeba641ba
commit 19e008b7ff
3 changed files with 130 additions and 42 deletions
@@ -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<MarketingStrategy> 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<String, Object> 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) { private StrategyHistoryResponse convertToStrategyHistoryResponse(MarketingStrategy strategy) {
StrategyHistoryResponse response = new StrategyHistoryResponse(); StrategyHistoryResponse response = new StrategyHistoryResponse();
response.setStrategyId(strategy.getId()); response.setStrategyId(strategy.getId());
@@ -42,7 +42,7 @@ public class MarketingStrategyV3Service {
@Value("${openai.model.name.text:gpt-4o}") @Value("${openai.model.name.text:gpt-4o}")
private String highIntelligenceModel; private String highIntelligenceModel;
@Value("${image.generation.delayBetweenRequestsMs:3000}") @Value("${image.generation.delayBetweenRequestsMs:5000}") // Увеличили задержку до 5 сек
private long delayBetweenRequestsMs; private long delayBetweenRequestsMs;
public Optional<MarketingStrategy> getStrategyById(String id) { public Optional<MarketingStrategy> getStrategyById(String id) {
@@ -205,6 +205,9 @@ public class MarketingStrategyV3Service {
MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex);
// Убеждаемся, что тип контента фото, если регенерируем фото
item.setContentType("фото");
Optional<MarketingAnalysisV3Document> analysisOpt = analysisRepository.findById(strategy.getAnalysisId()); Optional<MarketingAnalysisV3Document> analysisOpt = analysisRepository.findById(strategy.getAnalysisId());
if (analysisOpt.isEmpty()) { if (analysisOpt.isEmpty()) {
return null; return null;
@@ -234,6 +237,40 @@ public class MarketingStrategyV3Service {
return item; return item;
} }
// НОВЫЙ МЕТОД ДЛЯ РЕГЕНЕРАЦИИ ВИДЕО
public MarketingStrategy.PostCalendarItem regeneratePostVideo(String strategyId, int postIndex) {
Optional<MarketingStrategy> optStrategy = repository.findById(strategyId);
if (optStrategy.isEmpty()) {
return null;
}
MarketingStrategy strategy = optStrategy.get();
List<MarketingStrategy.PostCalendarItem> postCalendar = strategy.getPostCalendar();
if (postCalendar == null || postIndex < 0 || postIndex >= postCalendar.size()) {
return null;
}
MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex);
// Меняем тип контента на видео
item.setContentType("видео");
Optional<MarketingAnalysisV3Document> 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) { public StrategyModel calculateBestScoringModel(kz.konturai.parser.dto.MarketingAnalysisV3Request req) {
int entry = 0, authority = 0, trust = 0, conversion = 0; int entry = 0, authority = 0, trust = 0, conversion = 0;
@@ -366,24 +403,7 @@ public class MarketingStrategyV3Service {
try { try {
if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) { 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."; doGenerateVideoWithFallback(item, businessContext, brandName, currentRefBytes);
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);
}
} else { } else {
doGenerateImage(item, businessContext, brandName, currentRefBytes); 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) { private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientRefBytes) {
String imagePrompt = buildImagePrompt(item, businessContext, brandName); String imagePrompt = buildImagePrompt(item, businessContext, brandName);
try { try {
@@ -405,6 +469,9 @@ public class MarketingStrategyV3Service {
minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE); minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE);
item.setImageUrl(filename); item.setImageUrl(filename);
item.setImageFilename(filename); item.setImageFilename(filename);
// Очищаем данные видео, так как теперь это фото
item.setVideoUrl(null);
item.setVideoFilename(null);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Image generation failed: {}", e.getMessage()); log.error("Image generation failed: {}", e.getMessage());
@@ -435,16 +502,11 @@ public class MarketingStrategyV3Service {
prompt.append("Context: ").append(businessContext).append(". "); 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()) { if (item.getTheme() != null && !item.getTheme().isEmpty()) {
prompt.append("Main subject/action: ").append(item.getTheme()).append(". "); 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("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(); return prompt.toString();
} }
@@ -106,14 +106,12 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model);
String finalPrompt = sanitizePromptForText(prompt); String finalPrompt = sanitizePromptForText(prompt);
// Первая попытка: с логотипом (если он есть)
try { try {
Map<String, Object> requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes); Map<String, Object> requestBody = buildImagenRequestBody(finalPrompt, referenceImageBytes);
Map<String, Object> response = executeVertexRequest(endpointUrl, accessToken, requestBody); Map<String, Object> response = executeVertexRequest(endpointUrl, accessToken, requestBody);
lastRequestTimestamp = System.currentTimeMillis(); lastRequestTimestamp = System.currentTimeMillis();
return extractImageFromImagenResponse(response); return extractImageFromImagenResponse(response);
} catch (WebClientResponseException.BadRequest badRequestEx) { } catch (WebClientResponseException.BadRequest badRequestEx) {
// Если API вернул 400 Bad Request из-за кривого референса, пробуем сгенерировать БЕЗ него
if (referenceImageBytes != null && referenceImageBytes.length > 0) { if (referenceImageBytes != null && referenceImageBytes.length > 0) {
logger.warn("Vertex AI rejected the request with reference image (400 Bad Request). Retrying WITHOUT reference image..."); logger.warn("Vertex AI rejected the request with reference image (400 Bad Request). Retrying WITHOUT reference image...");
Map<String, Object> fallbackBody = buildImagenRequestBody(finalPrompt, null); Map<String, Object> fallbackBody = buildImagenRequestBody(finalPrompt, null);
@@ -165,12 +163,9 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
private String sanitizePromptForText(String originalPrompt) { private String sanitizePromptForText(String originalPrompt) {
if (originalPrompt == null) return "High quality, photorealistic image"; if (originalPrompt == null) return "High quality, photorealistic image";
String sanitized = originalPrompt.replaceAll("(?i)integrate the brand name '[^']+'", "integrate the provided logo image"); return originalPrompt; // Убрали лишнюю очистку, которая могла ломать логику ИИ
sanitized = sanitized.replaceAll("(?i)with the text '[^']+'", "with the provided logo");
return sanitized;
} }
// ИСПРАВЛЕННЫЙ МЕТОД ФОРМИРОВАНИЯ JSON
private Map<String, Object> buildImagenRequestBody(String prompt, byte[] referenceImageBytes) { private Map<String, Object> buildImagenRequestBody(String prompt, byte[] referenceImageBytes) {
Map<String, Object> body = new HashMap<>(); Map<String, Object> body = new HashMap<>();
List<Map<String, Object>> instances = new ArrayList<>(); List<Map<String, Object>> instances = new ArrayList<>();
@@ -178,7 +173,6 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
instance.put("prompt", prompt); instance.put("prompt", prompt);
// Правильная структура referenceImage для Imagen 3
if (referenceImageBytes != null && referenceImageBytes.length > 0) { if (referenceImageBytes != null && referenceImageBytes.length > 0) {
String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes); String base64Image = Base64.getEncoder().encodeToString(referenceImageBytes);
@@ -186,22 +180,17 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
Map<String, Object> imageBytesMap = new HashMap<>(); Map<String, Object> imageBytesMap = new HashMap<>();
imageBytesMap.put("bytesBase64Encoded", base64Image); imageBytesMap.put("bytesBase64Encoded", base64Image);
// Желательно указывать MIME тип, чтобы Imagen не путался // Убрали mimeType, чтобы избежать 400 Bad Request из-за несоответствия типов
imageBytesMap.put("mimeType", "image/png");
referenceImageParams.put("referenceImage", imageBytesMap); referenceImageParams.put("referenceImage", imageBytesMap);
// Для интеграции логотипа/предмета в сцену Google рекомендует использовать "SUBJECT" или не указывать тип вообще // Возвращаем STYLE, так как он меньше ломает геометрию
// В v1beta API структура referenceImages немного отличается. referenceImageParams.put("referenceType", "STYLE");
referenceImageParams.put("referenceType", "SUBJECT");
// Для Imagen 3 referenceImages передается на уровне instance, а не параметров
List<Map<String, Object>> refImagesList = new ArrayList<>(); List<Map<String, Object>> refImagesList = new ArrayList<>();
refImagesList.add(referenceImageParams); refImagesList.add(referenceImageParams);
instance.put("referenceImages", refImagesList); instance.put("referenceImages", refImagesList);
instance.put("prompt", prompt + ". Use the provided reference image.");
// Если используем SUBJECT, промпт должен четко описывать сцену вокруг этого объекта
instance.put("prompt", prompt + ". The image must feature the exact provided reference object/logo naturally integrated into the scene.");
} }
instances.add(instance); instances.add(instance);
@@ -212,8 +201,8 @@ public class NanoBananaImageGenerationService implements ImageGenerationService
parameters.put("aspectRatio", "1:1"); parameters.put("aspectRatio", "1:1");
parameters.put("safetyFilterLevel", "block_some"); parameters.put("safetyFilterLevel", "block_some");
parameters.put("personGeneration", "allow_adult"); parameters.put("personGeneration", "allow_adult");
// Жесткий негативный промпт // Убрали text из негативного промпта
parameters.put("negativePrompt", "text, typography, letters, words, writing, signature, watermark, nsfw, nudity, violence, deformed, ugly, blurry, distorted"); parameters.put("negativePrompt", "nsfw, nudity, violence, deformed, ugly, blurry, distorted");
body.put("parameters", parameters); body.put("parameters", parameters);
return body; return body;