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);
addStatusHistoryEntry(strategy, "queued", "Запуск генерации. ИИ подбирает оптимальные платформы...");
addStatusHistoryEntry(strategy, "queued", "Запуск генерации. ИИ проектирует массированный контент-план...");
strategy = repository.save(strategy);
processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
@@ -79,7 +79,7 @@ public class MarketingStrategyV3Service {
public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis, MarketingStrategy strategy) {
try {
strategy.setStatus("processing");
addStatusHistoryEntry(strategy, "processing", "Расчет баллов, выбор платформ и генерация контент-плана...");
addStatusHistoryEntry(strategy, "processing", "Расчет баллов, генерация постов (минимум 3 в неделю)...");
repository.save(strategy);
// 1. Считаем лучшую модель
@@ -108,7 +108,10 @@ public class MarketingStrategyV3Service {
populateStrategyEntity(strategy, strategyData);
repository.save(strategy);
// 5. Генерируем медиа НАДЕЖНЫМ способом из V1
// 5. Генерируем медиа НАДЕЖНЫМ способом с FALLBACK
addStatusHistoryEntry(strategy, "processing", "Создание фото и видео креативов. Это займет несколько минут...");
repository.save(strategy);
generateMediaAssets(strategy, analysis);
strategy.setStatus("completed");
@@ -122,6 +125,7 @@ public class MarketingStrategyV3Service {
}
}
// Тот самый надежный калькулятор
public StrategyModel calculateBestScoringModel(kz.konturai.parser.dto.MarketingAnalysisV3Request req) {
int entry = 0, authority = 0, trust = 0, conversion = 0;
@@ -201,16 +205,15 @@ public class MarketingStrategyV3Service {
for (Map<String, Object> c : calendarMap) {
MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem();
// Безопасный парсинг даты как в V1
String dateStr = (String) c.get("publishDate");
if (dateStr != null) {
try {
item.setPublishDate(LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
} catch (Exception e) {
item.setPublishDate(startDate.plusDays(postCalendar.size()));
item.setPublishDate(startDate.plusDays(postCalendar.size() * 2L)); // Распределяем по дням
}
} else {
item.setPublishDate(startDate.plusDays(postCalendar.size()));
item.setPublishDate(startDate.plusDays(postCalendar.size() * 2L));
}
item.setPlatform((String) c.get("platform"));
@@ -227,7 +230,7 @@ public class MarketingStrategyV3Service {
}
// ==========================================
// НАДЕЖНАЯ ГЕНЕРАЦИЯ МЕДИА (ПОРТИРОВАНО ИЗ V1)
// ЖЕЛЕЗОБЕТОННАЯ ГЕНЕРАЦИЯ МЕДИА С FALLBACK
// ==========================================
private void generateMediaAssets(MarketingStrategy strategy, MarketingAnalysisV3Document analysis) {
@@ -241,93 +244,114 @@ public class MarketingStrategyV3Service {
InputStream logoStream = minIOService.downloadFile(logoFilename);
clientLogoBytes = logoStream.readAllBytes();
logoStream.close();
log.info("Загружен логотип: {}", logoFilename);
log.info("Загружен логотип для генерации: {}", logoFilename);
} catch (Exception e) {
log.error("Не удалось скачать логотип {}: {}", logoFilename, e.getMessage());
}
}
for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
String contentType = item.getContentType() != null ? item.getContentType().toLowerCase() : "";
String contentType = item.getContentType() != null ? item.getContentType().toLowerCase() : "фото";
try {
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) {
String filename = "video_" + System.currentTimeMillis() + "_" + item.hashCode() + ".mp4";
minIOService.uploadFile(filename, videoBytes, "video/mp4");
item.setVideoUrl(filename);
item.setVideoFilename(filename);
log.info("Видео успешно сгенерировано: {}", filename);
} else {
// ФОЛБЭК: ВИДЕО НЕ ПОЛУЧИЛОСЬ -> ДЕЛАЕМ ФОТО
log.warn("Видео не сгенерировалось (вернулся null). Включаю FALLBACK -> генерирую ФОТО для темы: {}", item.getTheme());
item.setContentType("фото"); // Меняем тип поста, чтобы фронт отобразил картинку
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
}
} else {
// ИСПОЛЬЗУЕМ СТАРЫЙ ДОБРЫЙ BUILDER ПРОМПТОВ!
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
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);
}
// ГЕНЕРИРУЕМ ФОТО
doGenerateImage(item, businessContext, brandName, clientLogoBytes);
}
if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs);
} 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) {
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) {
StringBuilder prompt = new StringBuilder();
String style = "высококачественная фотография, реалистичное освещение, 8k, высокая детализация";
String style = "high-end commercial photography, photorealistic, 8k resolution, highly detailed, professional lighting";
if (item.getPlatform() != null) {
String platform = item.getPlatform().toLowerCase();
if (platform.contains("linkedin")) {
style += ", корпоративный стиль, профессионально, минимализм, чисто";
style += ", corporate style, professional, minimalist, clean";
} else if (platform.contains("instagram") || platform.contains("tiktok")) {
style += ", лайфстайл, эстетика, яркие цвета, привлекательно, инстаграм-стиль";
} else if (platform.contains("facebook")) {
style += ", уютно, дружелюбно, для сообщества";
style += ", lifestyle, aesthetic, vibrant colors, engaging, instagram-style";
}
}
prompt.append("Фотореалистичное изображение. ");
prompt.append(style).append(". ");
if (businessContext != null && !businessContext.isEmpty()) {
prompt.append("Контекст: ").append(businessContext).append(". ");
prompt.append("Context: ").append(businessContext).append(". ");
}
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()) {
prompt.append("Сюжет изображения: ").append(item.getTheme()).append(". ");
// Переводим тему на английский контекст (ИИ поймет)
prompt.append("Main subject/action: ").append(item.getTheme()).append(". ");
}
prompt.append("Композиция: продукт или услуга в центре внимания. ");
prompt.append("Если на фото есть люди, они должны взаимодействовать с продуктом естественно. ");
prompt.append("Избегать: абстрактных концепций, искусственных поз, размытия, искажений. ");
prompt.append("Визуальный стиль: ").append(style).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();
}
// ==========================================
// ПРОМПТЫ ДЛЯ ИИ (ТЕПЕРЬ БЕЗ ПРОМПТОВ ДЛЯ КАРТИНОК В JSON)
// ПРОМПТЫ ДЛЯ ИИ (СТРОГИЕ ПРАВИЛА КОЛИЧЕСТВА)
// ==========================================
private String buildSystemPrompt(StrategyModel bestModel) {
@@ -336,9 +360,14 @@ public class MarketingStrategyV3Service {
Бэкенд-система произвела расчет и выбрала доминирующую стратегию: %s.
Твоя задача — проанализировать нишу и САМОСТОЯТЕЛЬНО:
1. Выбрать наиболее эффективные платформы (от 1 до 3 платформ из: Instagram, Facebook, LinkedIn, Telegram, TikTok, YouTube).
2. Определить оптимальную длительность первой итерации стратегии (от 2 до 8 недель).
3. Сгенерировать контент-план на основе твоих решений.
1. Выбрать наиболее эффективные платформы (от 1 до 3 шт).
2. Определить оптимальную длительность стратегии (рекомендую 4 недели).
3. Сгенерировать МАСШТАБНЫЙ контент-план.
КРИТИЧЕСКОЕ ПРАВИЛО МАТЕМАТИКИ:
Ты ОБЯЗАН сгенерировать МИНИМУМ 3 поста на КАЖДУЮ неделю.
Если ты выбрал длительность 4 недели, в массиве 'postCalendar' должно быть РОВНО 12 постов (или больше).
Обязательно чередуй типы контента: 'фото' и 'видео'.
ПРАВИЛА МОДЕЛИ:
%s
@@ -351,7 +380,6 @@ public class MarketingStrategyV3Service {
String clientDtoJson = objectMapper.writeValueAsString(analysis.getRequestData());
LocalDateTime startDate = LocalDateTime.now().plusDays(1);
// УБРАЛИ imageGenerationPrompt ИЗ ШАБЛОНА, ЧТОБЫ ИИ НЕ ЛОМАЛ JSON!
return """
ДАННЫЕ БИЗНЕСА:
%s
@@ -359,16 +387,16 @@ public class MarketingStrategyV3Service {
Старт публикаций: %s
ВЫБРАННАЯ МОДЕЛЬ: %s
ВЫВЕДИ ТОЛЬКО JSON СТРОГО ПО ЭТОЙ СТРУКТУРЕ:
ВЫВЕДИ ТОЛЬКО JSON СТРОГО ПО ЭТОЙ СТРУКТУРЕ (СГЕНЕРИРУЙ 12 ПОСТОВ ЕСЛИ ВЫБРАЛ 4 НЕДЕЛИ):
{
"selectedModel": "%s",
"rationale": "Обоснование выбора платформ",
"recommendedDurationWeeks": 4,
"recommendedPlatforms": ["Instagram"],
"recommendedPlatforms": ["Instagram", "TikTok"],
"weeklyPlans": [
{
"weekNumber": 1,
"mainThemes": ["Тема 1"],
"mainThemes": ["Тема 1", "Тема 2", "Тема 3"],
"contentRecommendations": "Что снимать и писать",
"priorityPlatforms": ["Instagram"]
}
@@ -378,10 +406,19 @@ public class MarketingStrategyV3Service {
"publishDate": "2024-01-15T10:00:00",
"platform": "Instagram",
"contentType": "фото",
"theme": "Тема поста",
"theme": "Тема первого поста",
"postText": "Текст поста...",
"hashtags": ["#тег"],
"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) {
if (response == null || response.trim().isEmpty()) return null;
String cleaned = response.trim();
// Надежное извлечение как в старом коде
int startIdx = cleaned.indexOf("{");
int endIdx = cleaned.lastIndexOf("}");