This commit is contained in:
arys
2026-02-28 20:44:51 +05:00
parent 369c85430f
commit 991a4213e6
@@ -67,7 +67,7 @@ public class MarketingStrategyV3Service {
} }
strategy.setStrategyData(initialData); strategy.setStrategyData(initialData);
addStatusHistoryEntry(strategy, "queued", "Запуск генерации. ИИ подбирает оптимальные платформы..."); addStatusHistoryEntry(strategy, "queued", "Запуск генерации. ИИ проектирует массированный контент-план...");
strategy = repository.save(strategy); strategy = repository.save(strategy);
processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy); processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
@@ -79,7 +79,7 @@ public class MarketingStrategyV3Service {
public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis, MarketingStrategy strategy) { public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis, MarketingStrategy strategy) {
try { try {
strategy.setStatus("processing"); strategy.setStatus("processing");
addStatusHistoryEntry(strategy, "processing", "Расчет баллов, выбор платформ и генерация контент-плана..."); addStatusHistoryEntry(strategy, "processing", "Расчет баллов, генерация постов (минимум 3 в неделю)...");
repository.save(strategy); repository.save(strategy);
// 1. Считаем лучшую модель // 1. Считаем лучшую модель
@@ -108,7 +108,10 @@ public class MarketingStrategyV3Service {
populateStrategyEntity(strategy, strategyData); populateStrategyEntity(strategy, strategyData);
repository.save(strategy); repository.save(strategy);
// 5. Генерируем медиа НАДЕЖНЫМ способом из V1 // 5. Генерируем медиа НАДЕЖНЫМ способом с FALLBACK
addStatusHistoryEntry(strategy, "processing", "Создание фото и видео креативов. Это займет несколько минут...");
repository.save(strategy);
generateMediaAssets(strategy, analysis); generateMediaAssets(strategy, analysis);
strategy.setStatus("completed"); strategy.setStatus("completed");
@@ -122,6 +125,7 @@ public class MarketingStrategyV3Service {
} }
} }
// Тот самый надежный калькулятор
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;
@@ -201,16 +205,15 @@ public class MarketingStrategyV3Service {
for (Map<String, Object> c : calendarMap) { for (Map<String, Object> c : calendarMap) {
MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem(); MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem();
// Безопасный парсинг даты как в V1
String dateStr = (String) c.get("publishDate"); String dateStr = (String) c.get("publishDate");
if (dateStr != null) { if (dateStr != null) {
try { try {
item.setPublishDate(LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME)); item.setPublishDate(LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} catch (Exception e) { } catch (Exception e) {
item.setPublishDate(startDate.plusDays(postCalendar.size())); item.setPublishDate(startDate.plusDays(postCalendar.size() * 2L)); // Распределяем по дням
} }
} else { } else {
item.setPublishDate(startDate.plusDays(postCalendar.size())); item.setPublishDate(startDate.plusDays(postCalendar.size() * 2L));
} }
item.setPlatform((String) c.get("platform")); item.setPlatform((String) c.get("platform"));
@@ -227,7 +230,7 @@ public class MarketingStrategyV3Service {
} }
// ========================================== // ==========================================
// НАДЕЖНАЯ ГЕНЕРАЦИЯ МЕДИА (ПОРТИРОВАНО ИЗ V1) // ЖЕЛЕЗОБЕТОННАЯ ГЕНЕРАЦИЯ МЕДИА С FALLBACK
// ========================================== // ==========================================
private void generateMediaAssets(MarketingStrategy strategy, MarketingAnalysisV3Document analysis) { private void generateMediaAssets(MarketingStrategy strategy, MarketingAnalysisV3Document analysis) {
@@ -241,93 +244,114 @@ public class MarketingStrategyV3Service {
InputStream logoStream = minIOService.downloadFile(logoFilename); InputStream logoStream = minIOService.downloadFile(logoFilename);
clientLogoBytes = logoStream.readAllBytes(); clientLogoBytes = logoStream.readAllBytes();
logoStream.close(); logoStream.close();
log.info("Загружен логотип: {}", logoFilename); log.info("Загружен логотип для генерации: {}", logoFilename);
} catch (Exception e) { } catch (Exception e) {
log.error("Не удалось скачать логотип {}: {}", logoFilename, e.getMessage()); log.error("Не удалось скачать логотип {}: {}", logoFilename, e.getMessage());
} }
} }
for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) { for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
String contentType = item.getContentType() != null ? item.getContentType().toLowerCase() : ""; String contentType = item.getContentType() != null ? item.getContentType().toLowerCase() : "фото";
try { try {
if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) { if (contentType.contains("видео") || contentType.contains("reels") || contentType.contains("tiktok")) {
String videoPrompt = "Кинематографичное видео. Бизнес: " + businessContext + ". Сюжет: " + item.getTheme(); // ПРОБУЕМ СГЕНЕРИРОВАТЬ ВИДЕО
byte[] videoBytes = geminiVideoService.generateVideo(videoPrompt); String videoPrompt = "High quality cinematic commercial video. Business niche: " + businessContext + ". Scene: " + item.getTheme() + ". Photorealistic, dynamic motion, 4k.";
log.info("Попытка генерации ВИДЕО: {}", videoPrompt);
byte[] videoBytes = null;
try {
videoBytes = geminiVideoService.generateVideo(videoPrompt);
} catch (Exception e) {
log.warn("Сервис видео упал с ошибкой: {}", e.getMessage());
}
if (videoBytes != null && videoBytes.length > 0) { if (videoBytes != null && videoBytes.length > 0) {
String filename = "video_" + System.currentTimeMillis() + "_" + item.hashCode() + ".mp4"; String filename = "video_" + System.currentTimeMillis() + "_" + item.hashCode() + ".mp4";
minIOService.uploadFile(filename, videoBytes, "video/mp4"); minIOService.uploadFile(filename, videoBytes, "video/mp4");
item.setVideoUrl(filename); item.setVideoUrl(filename);
item.setVideoFilename(filename); item.setVideoFilename(filename);
log.info("Видео успешно сгенерировано: {}", filename);
} else {
// ФОЛБЭК: ВИДЕО НЕ ПОЛУЧИЛОСЬ -> ДЕЛАЕМ ФОТО
log.warn("Видео не сгенерировалось (вернулся null). Включаю FALLBACK -> генерирую ФОТО для темы: {}", item.getTheme());
item.setContentType("фото"); // Меняем тип поста, чтобы фронт отобразил картинку
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
} }
} else { } else {
// ИСПОЛЬЗУЕМ СТАРЫЙ ДОБРЫЙ BUILDER ПРОМПТОВ! // ГЕНЕРИРУЕМ ФОТО
String imagePrompt = buildImagePrompt(item, businessContext, brandName); doGenerateImage(item, businessContext, brandName, clientLogoBytes);
log.info("Generating image with prompt: {}", imagePrompt);
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientLogoBytes);
if (imageBytes != null && imageBytes.length > 0) {
String filename = "image_" + System.currentTimeMillis() + "_" + item.hashCode() + ".png";
minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG.toString());
item.setImageUrl(filename);
item.setImageFilename(filename);
}
} }
if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs); if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs);
} catch (Exception e) { } catch (Exception e) {
log.error("Ошибка создания медиа для поста '{}': {}", item.getTheme(), e.getMessage()); log.error("Критическая ошибка создания медиа для поста '{}': {}", item.getTheme(), e.getMessage());
} }
} }
} }
// Выделенный надежный метод для фото
private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName, byte[] clientLogoBytes) {
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
log.info("Генерация ФОТО: {}", imagePrompt);
try {
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientLogoBytes);
if (imageBytes != null && imageBytes.length > 0) {
String filename = "image_" + System.currentTimeMillis() + "_" + item.hashCode() + ".png";
minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE);
item.setImageUrl(filename);
item.setImageFilename(filename);
} else {
log.warn("NanoBanana вернул пустой массив байтов для темы: {}", item.getTheme());
}
} catch (Exception e) {
log.error("Генерация фото упала с ошибкой: {}", e.getMessage());
}
}
private String getBusinessContext(MarketingAnalysisV3Document analysis) { private String getBusinessContext(MarketingAnalysisV3Document analysis) {
if (analysis == null || analysis.getRequestData() == null) return ""; if (analysis == null || analysis.getRequestData() == null) return "";
return "Ниша: " + analysis.getRequestData().getBusinessNiche() + ", Продукт: " + analysis.getRequestData().getProductName(); return analysis.getRequestData().getBusinessNiche() + ", product: " + analysis.getRequestData().getProductName();
} }
// ПРОМПТ ДЛЯ ФОТО (НА АНГЛИЙСКОМ ДЛЯ ЛУЧШЕГО КАЧЕСТВА)
private String buildImagePrompt(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName) { private String buildImagePrompt(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName) {
StringBuilder prompt = new StringBuilder(); StringBuilder prompt = new StringBuilder();
String style = "высококачественная фотография, реалистичное освещение, 8k, высокая детализация"; String style = "high-end commercial photography, photorealistic, 8k resolution, highly detailed, professional lighting";
if (item.getPlatform() != null) { if (item.getPlatform() != null) {
String platform = item.getPlatform().toLowerCase(); String platform = item.getPlatform().toLowerCase();
if (platform.contains("linkedin")) { if (platform.contains("linkedin")) {
style += ", корпоративный стиль, профессионально, минимализм, чисто"; style += ", corporate style, professional, minimalist, clean";
} else if (platform.contains("instagram") || platform.contains("tiktok")) { } else if (platform.contains("instagram") || platform.contains("tiktok")) {
style += ", лайфстайл, эстетика, яркие цвета, привлекательно, инстаграм-стиль"; style += ", lifestyle, aesthetic, vibrant colors, engaging, instagram-style";
} else if (platform.contains("facebook")) {
style += ", уютно, дружелюбно, для сообщества";
} }
} }
prompt.append("Фотореалистичное изображение. "); prompt.append(style).append(". ");
if (businessContext != null && !businessContext.isEmpty()) { if (businessContext != null && !businessContext.isEmpty()) {
prompt.append("Контекст: ").append(businessContext).append(". "); prompt.append("Context: ").append(businessContext).append(". ");
} }
if (brandName != null && !brandName.isEmpty()) { if (brandName != null && !brandName.isEmpty()) {
prompt.append("Органично и нативно интегрируй название бренда '").append(brandName).append("' в окружение (например, на вывеске, упаковке, кружке или блокноте). "); 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("Сюжет изображения: ").append(item.getTheme()).append(". "); // Переводим тему на английский контекст (ИИ поймет)
prompt.append("Main subject/action: ").append(item.getTheme()).append(". ");
} }
prompt.append("Композиция: продукт или услуга в центре внимания. "); prompt.append("Composition: Product or service is the main focus. People (if any) should look natural. ");
prompt.append("Если на фото есть люди, они должны взаимодействовать с продуктом естественно. "); prompt.append("Avoid: abstract concepts, people staring blankly at camera, fake poses, text overlay, blur, distortion.");
prompt.append("Избегать: абстрактных концепций, искусственных поз, размытия, искажений. ");
prompt.append("Визуальный стиль: ").append(style).append(".");
return prompt.toString(); return prompt.toString();
} }
// ========================================== // ==========================================
// ПРОМПТЫ ДЛЯ ИИ (ТЕПЕРЬ БЕЗ ПРОМПТОВ ДЛЯ КАРТИНОК В JSON) // ПРОМПТЫ ДЛЯ ИИ (СТРОГИЕ ПРАВИЛА КОЛИЧЕСТВА)
// ========================================== // ==========================================
private String buildSystemPrompt(StrategyModel bestModel) { private String buildSystemPrompt(StrategyModel bestModel) {
@@ -336,9 +360,14 @@ public class MarketingStrategyV3Service {
Бэкенд-система произвела расчет и выбрала доминирующую стратегию: %s. Бэкенд-система произвела расчет и выбрала доминирующую стратегию: %s.
Твоя задача — проанализировать нишу и САМОСТОЯТЕЛЬНО: Твоя задача — проанализировать нишу и САМОСТОЯТЕЛЬНО:
1. Выбрать наиболее эффективные платформы (от 1 до 3 платформ из: Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube). 1. Выбрать наиболее эффективные платформы (от 1 до 3 шт).
2. Определить оптимальную длительность первой итерации стратегии (от 2 до 8 недель). 2. Определить оптимальную длительность стратегии (рекомендую 4 недели).
3. Сгенерировать контент-план на основе твоих решений. 3. Сгенерировать МАСШТАБНЫЙ контент-план.
КРИТИЧЕСКОЕ ПРАВИЛО МАТЕМАТИКИ:
Ты ОБЯЗАН сгенерировать МИНИМУМ 3 поста на КАЖДУЮ неделю.
Если ты выбрал длительность 4 недели, в массиве 'postCalendar' должно быть РОВНО 12 постов (или больше).
Обязательно чередуй типы контента: 'фото' и 'видео'.
ПРАВИЛА МОДЕЛИ: ПРАВИЛА МОДЕЛИ:
%s %s
@@ -351,7 +380,6 @@ public class MarketingStrategyV3Service {
String clientDtoJson = objectMapper.writeValueAsString(analysis.getRequestData()); String clientDtoJson = objectMapper.writeValueAsString(analysis.getRequestData());
LocalDateTime startDate = LocalDateTime.now().plusDays(1); LocalDateTime startDate = LocalDateTime.now().plusDays(1);
// УБРАЛИ imageGenerationPrompt ИЗ ШАБЛОНА, ЧТОБЫ ИИ НЕ ЛОМАЛ JSON!
return """ return """
ДАННЫЕ БИЗНЕСА: ДАННЫЕ БИЗНЕСА:
%s %s
@@ -359,16 +387,16 @@ public class MarketingStrategyV3Service {
Старт публикаций: %s Старт публикаций: %s
ВЫБРАННАЯ МОДЕЛЬ: %s ВЫБРАННАЯ МОДЕЛЬ: %s
ВЫВЕДИ ТОЛЬКО JSON СТРОГО ПО ЭТОЙ СТРУКТУРЕ: ВЫВЕДИ ТОЛЬКО JSON СТРОГО ПО ЭТОЙ СТРУКТУРЕ (СГЕНЕРИРУЙ 12 ПОСТОВ ЕСЛИ ВЫБРАЛ 4 НЕДЕЛИ):
{ {
"selectedModel": "%s", "selectedModel": "%s",
"rationale": "Обоснование выбора платформ", "rationale": "Обоснование выбора платформ",
"recommendedDurationWeeks": 4, "recommendedDurationWeeks": 4,
"recommendedPlatforms": ["Instagram"], "recommendedPlatforms": ["Instagram", "TikTok"],
"weeklyPlans": [ "weeklyPlans": [
{ {
"weekNumber": 1, "weekNumber": 1,
"mainThemes": ["Тема 1"], "mainThemes": ["Тема 1", "Тема 2", "Тема 3"],
"contentRecommendations": "Что снимать и писать", "contentRecommendations": "Что снимать и писать",
"priorityPlatforms": ["Instagram"] "priorityPlatforms": ["Instagram"]
} }
@@ -378,10 +406,19 @@ public class MarketingStrategyV3Service {
"publishDate": "2024-01-15T10:00:00", "publishDate": "2024-01-15T10:00:00",
"platform": "Instagram", "platform": "Instagram",
"contentType": "фото", "contentType": "фото",
"theme": "Тема поста", "theme": "Тема первого поста",
"postText": "Текст поста...", "postText": "Текст поста...",
"hashtags": ["#тег"], "hashtags": ["#тег"],
"publishTime": "10:00" "publishTime": "10:00"
},
{
"publishDate": "2024-01-17T14:00:00",
"platform": "TikTok",
"contentType": "видео",
"theme": "Тема второго поста",
"postText": "Текст поста...",
"hashtags": ["#тег"],
"publishTime": "14:00"
} }
] ]
} }
@@ -393,15 +430,10 @@ public class MarketingStrategyV3Service {
); );
} }
// ==========================================
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ (ИЗ V1)
// ==========================================
private String extractCleanJson(String response) { private String extractCleanJson(String response) {
if (response == null || response.trim().isEmpty()) return null; if (response == null || response.trim().isEmpty()) return null;
String cleaned = response.trim(); String cleaned = response.trim();
// Надежное извлечение как в старом коде
int startIdx = cleaned.indexOf("{"); int startIdx = cleaned.indexOf("{");
int endIdx = cleaned.lastIndexOf("}"); int endIdx = cleaned.lastIndexOf("}");