fix
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user