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