diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java index 7d57172..9a8b7e2 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java @@ -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 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("}");