diff --git a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java index 69675d9..fd17eb5 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingAnalysisV3Controller.java @@ -501,7 +501,7 @@ public class MarketingAnalysisV3Controller { } } - // НОВЫЙ ЭНДПОИНТ ДЛЯ РЕГЕНЕРАЦИИ ВИДЕО + // ИСПРАВЛЕННЫЙ ЭНДПОИНТ ДЛЯ АСИНХРОННОЙ РЕГЕНЕРАЦИИ ВИДЕО @PostMapping("/strategy/{strategyId}/post/{postIndex}/regenerate-video") public ResponseEntity regeneratePostVideo( @RequestHeader(value = "Authorization", required = false) String authHeader, @@ -518,21 +518,16 @@ public class MarketingAnalysisV3Controller { MarketingStrategy strategy = optStrategy.get(); if (!strategy.getUserId().equals(userId)) return forbiddenResponse(); - MarketingStrategy.PostCalendarItem updatedItem = strategyService.regeneratePostVideo(strategyId, postIndex); - if (updatedItem == null) return notFoundResponse("Пост не найден"); + // Вызываем АСИНХРОННЫЙ метод сервиса + strategyService.regeneratePostVideoAsync(strategyId, postIndex); + // Мгновенно отдаем ответ фронту со статусом 202 Accepted Map 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()); + responseData.put("message", "Генерация видео запущена в фоновом режиме. Видео появится через несколько минут."); - return ResponseEntity.ok(ApiResponse.success("Видео для поста успешно регенерировано", responseData)); + return ResponseEntity.accepted().body(ApiResponse.success("Генерация видео запущена", responseData)); } catch (Exception e) { return internalErrorResponse(e); } diff --git a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java index f5e01d7..42226d4 100644 --- a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java @@ -1,5 +1,6 @@ package kz.konturai.parser.service; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.auth.oauth2.GoogleCredentials; import io.netty.resolver.DefaultAddressResolverGroup; import org.slf4j.Logger; @@ -16,7 +17,6 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti import reactor.netty.http.client.HttpClient; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -// ВНИМАНИЕ: Если у тебя Spring Boot 2.x, замени jakarta на javax import jakarta.annotation.PostConstruct; import java.io.IOException; import java.io.InputStream; @@ -31,9 +31,8 @@ public class GeminiVideoGenerationService { private static final String CREDENTIALS_FILE_PATH = "keys/google-key.json"; private final WebClient webClient; - - // Кэшируем credentials, чтобы не дергать диск и Google API каждые 15 секунд private GoogleCredentials credentials; + private final ObjectMapper objectMapper = new ObjectMapper(); @Value("${google.cloud.project-id}") private String projectId; @@ -41,17 +40,17 @@ public class GeminiVideoGenerationService { @Value("${google.cloud.location:us-central1}") private String location; - // Модель Veo 2.0 @Value("${google.gemini.video.model:veo-2.0-generate-001}") private String model; public GeminiVideoGenerationService() { HttpClient httpClient = HttpClient.create() .resolver(DefaultAddressResolverGroup.INSTANCE) - .responseTimeout(Duration.ofMinutes(15)); // Долгий таймаут для видео + .responseTimeout(Duration.ofMinutes(15)); + // Лимит увеличен до 300МБ, чтобы 4K видео точно поместилось в памяти ExchangeStrategies strategies = ExchangeStrategies.builder() - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(200 * 1024 * 1024)) // 200MB + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(300 * 1024 * 1024)) .build(); this.webClient = WebClient.builder() @@ -66,37 +65,32 @@ public class GeminiVideoGenerationService { ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH); if (resource.exists()) { try (InputStream is = resource.getInputStream()) { - // Читаем ключ 1 раз при старте приложения! this.credentials = GoogleCredentials.fromStream(is) .createScoped(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform")); - logger.info("VEO: Учетные данные Google успешно загружены."); + logger.info("VEO: Учетные данные загружены. Модель={}", model); } } else { - logger.error("VEO: КРИТИЧЕСКАЯ ОШИБКА. Файл ключа не найден по пути: {}", CREDENTIALS_FILE_PATH); + logger.error("VEO: КРИТИЧЕСКАЯ ОШИБКА. Файл {} не найден!", CREDENTIALS_FILE_PATH); } } catch (IOException e) { - logger.error("VEO: Ошибка парсинга ключа Google: {}", e.getMessage()); + logger.error("VEO: Ошибка чтения ключа: {}", e.getMessage()); } } private String getValidAccessToken() { - if (this.credentials == null) { - logger.error("VEO: Учетные данные не инициализированы. Проверьте google-key.json"); - return null; - } + if (this.credentials == null) return null; try { - // Если токен истек, он обновится. Если нет — отдаст текущий. Никакого спама в API. this.credentials.refreshIfExpired(); return this.credentials.getAccessToken().getTokenValue(); } catch (IOException e) { - logger.error("VEO: Ошибка получения/обновления токена: {}", e.getMessage()); + logger.error("VEO: Ошибка обновления токена: {}", e.getMessage()); return null; } } public byte[] generateVideo(String prompt) { if (projectId == null || projectId.trim().isEmpty()) { - logger.error("VEO: Не указан Project ID (google.cloud.project-id)."); + logger.error("VEO: Не указан Project ID."); return null; } @@ -104,17 +98,21 @@ public class GeminiVideoGenerationService { if (accessToken == null) return null; try { - // ШАГ 1: Отправляем запрос на генерацию + // ШАГ 1: Запуск генерации String generateEndpoint = String.format( "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predictLongRunning", location, projectId, location, model ); + // Оставляем ТОЛЬКО формат видео. Лишние параметры крашат Google API. + Map parameters = new HashMap<>(); + parameters.put("aspectRatio", "9:16"); + Map requestBody = new HashMap<>(); requestBody.put("instances", Collections.singletonList(Collections.singletonMap("prompt", prompt))); - requestBody.put("parameters", Collections.singletonMap("aspectRatio", "9:16")); + requestBody.put("parameters", parameters); - logger.info("VEO: Запускаем генерацию видео. Промпт: '{}'", prompt); + logger.info("VEO: Отправка промпта в нейросеть: '{}'", prompt); Map initResponse = webClient.post() .uri(URI.create(generateEndpoint)) @@ -123,86 +121,109 @@ public class GeminiVideoGenerationService { .bodyValue(requestBody) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) - .block(Duration.ofSeconds(30)); + .block(Duration.ofSeconds(120)); if (initResponse == null || !initResponse.containsKey("name")) { - logger.error("VEO: Сервер Google не вернул operation name. Ответ: {}", initResponse); + logger.error("VEO: Ошибка: Google не вернул имя операции."); return null; } String operationName = (String) initResponse.get("name"); - logger.info("VEO: Задача успешно принята. ID операции: {}", operationName); + logger.info("VEO: Задача поставлена в очередь видеокарт Google."); + + // ВАЖНО: Ждем 35 секунд перед первой проверкой, чтобы Google успел создать задачу в своей базе. + // ЭТО УБЕРЕТ ОШИБКУ 404! + logger.info("VEO: Инициализация рендеринга... Ожидание 35 секунд перед проверкой статуса."); + Thread.sleep(35000); + + // ШАГ 2: Мягкий опрос статуса + String fetchEndpoint = String.format("https://%s-aiplatform.googleapis.com/v1/%s", location, operationName); - // ШАГ 2: Опрос статуса задачи - String statusEndpoint = String.format("https://%s-aiplatform.googleapis.com/v1/%s", location, operationName); int attempts = 0; - int maxAttempts = 60; // 60 * 15 сек = 15 минут + int maxAttempts = 80; // 20 минут максимум while (attempts < maxAttempts) { - Thread.sleep(15000); // Ожидание 15 секунд attempts++; - logger.info("VEO: Проверка статуса (попытка {} из {})...", attempts, maxAttempts); + logger.info("VEO: Рендеринг видео... Проверка статуса ({} из {}). Пожалуйста, не выключайте сервер!", attempts, maxAttempts); - // Берем актуальный токен (обновится сам, если прошло > 1 часа) String currentToken = getValidAccessToken(); + if (currentToken == null) continue; - Map statusResponse = webClient.get() - .uri(URI.create(statusEndpoint)) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}) - .block(Duration.ofSeconds(30)); + try { + Map statusResponse = webClient.get() + .uri(URI.create(fetchEndpoint)) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .block(Duration.ofSeconds(60)); + + if (statusResponse == null) continue; - if (statusResponse != null) { if (statusResponse.containsKey("error")) { - logger.error("VEO: Ошибка внутри процесса генерации: {}", statusResponse.get("error")); + logger.error("VEO: ОШИБКА ВНУТРИ GOOGLE: {}", objectMapper.writeValueAsString(statusResponse.get("error"))); return null; } if (Boolean.TRUE.equals(statusResponse.get("done"))) { - logger.info("VEO: Видео готово! Извлекаем байты..."); + logger.info("VEO: ВИДЕО УСПЕШНО СГЕНЕРИРОВАНО! Сохраняем файл..."); @SuppressWarnings("unchecked") - Map responseData = (Map) statusResponse.get("response"); - return extractVideoBytes(responseData); + Map responseObj = (Map) statusResponse.get("response"); + return extractVideoBytes(responseObj); } + + } catch (WebClientResponseException e) { + // Глушим любые промежуточные ошибки связи, просто ждем дальше + logger.debug("VEO: Техническая задержка связи с Google ({}). Идем на следующий круг...", e.getStatusCode()); } + + // Ждем 15 секунд перед следующей проверкой + Thread.sleep(15000); } - logger.error("VEO: Превышено время ожидания рендеринга (15 минут)."); + logger.error("VEO: Превышено время ожидания рендеринга (20 минут)."); return null; - } catch (WebClientResponseException e) { - logger.error("VEO: HTTP Ошибка API Google! Статус: {}. Тело: {}", e.getStatusCode(), e.getResponseBodyAsString()); - return null; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); // Восстанавливаем статус прерывания - logger.error("VEO: Поток ожидания был прерван", e); - return null; } catch (Exception e) { - logger.error("VEO: Неизвестная ошибка: {}", e.getMessage(), e); + logger.error("VEO: Фатальная ошибка сервиса: {}", e.getMessage(), e); return null; } } - @SuppressWarnings("unchecked") - private byte[] extractVideoBytes(Map responseData) { - if (responseData == null) return null; + private byte[] extractVideoBytes(Map data) { + if (data == null) return null; try { - List> predictions = (List>) responseData.get("predictions"); - if (predictions != null && !predictions.isEmpty()) { - Map firstPrediction = predictions.get(0); - String base64Video = (String) firstPrediction.get("bytesBase64Encoded"); - - if (base64Video != null) { - return Base64.getDecoder().decode(base64Video); - } else { - logger.error("VEO: В ответе нет поля bytesBase64Encoded! Структура ответа: {}", firstPrediction); + String b64 = findFirstValue(data, "bytesBase64Encoded"); + if (b64 != null && !b64.isEmpty()) { + logger.info("VEO: Получены байты видео (размер кода: {}). Декодируем...", b64.length()); + String cleanB64 = b64.replaceAll("\\s", ""); + if (cleanB64.contains("base64,")) { + cleanB64 = cleanB64.substring(cleanB64.indexOf("base64,") + 7); } - } else { - logger.error("VEO: Массив predictions пустой!"); + return Base64.getDecoder().decode(cleanB64); } + logger.error("VEO: Видео не найдено в ответе! Дамп: {}", objectMapper.writeValueAsString(data)); } catch (Exception e) { - logger.error("VEO: Ошибка при декодировании Base64 видео: {}", e.getMessage(), e); + logger.error("VEO: Ошибка извлечения байтов: {}", e.getMessage()); + } + return null; + } + + private String findFirstValue(Object obj, String targetKey) { + if (obj instanceof Map) { + Map map = (Map) obj; + if (map.containsKey(targetKey)) { + Object val = map.get(targetKey); + if (val instanceof String) return (String) val; + } + for (Object value : map.values()) { + String found = findFirstValue(value, targetKey); + if (found != null) return found; + } + } else if (obj instanceof List) { + for (Object item : (List) obj) { + String found = findFirstValue(item, targetKey); + if (found != null) return found; + } } return null; } diff --git a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java index 088240e..dde130e 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java +++ b/src/main/java/kz/konturai/parser/service/MarketingStrategyV3Service.java @@ -64,7 +64,8 @@ public class MarketingStrategyV3Service { return repository.findByUserIdOrderByCreatedAtDesc(userId); } - public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, List referenceFilenames) { + public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, + List referenceFilenames) { MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId) .orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found")); @@ -91,7 +92,8 @@ public class MarketingStrategyV3Service { } @Async("reportGenerationExecutor") - public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis, MarketingStrategy strategy) { + public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis, + MarketingStrategy strategy) { try { strategy.setStatus("processing"); addStatusHistoryEntry(strategy, "processing", "Расчет баллов, генерация постов (минимум 3 в неделю)..."); @@ -102,14 +104,16 @@ public class MarketingStrategyV3Service { String systemPrompt = buildSystemPrompt(bestModel); String userPrompt = buildUserPrompt(analysis, bestModel); - String rawResponse = aiService.generateWithInstructionWithModel("{}", userPrompt, "ru", highIntelligenceModel, systemPrompt, 16000, 240000L); + String rawResponse = aiService.generateWithInstructionWithModel("{}", userPrompt, "ru", + highIntelligenceModel, systemPrompt, 16000, 240000L); String jsonResponse = extractCleanJson(rawResponse); if (jsonResponse == null) { throw new IllegalStateException("AI failed to return valid JSON"); } - Map strategyData = objectMapper.readValue(jsonResponse, new TypeReference<>() {}); + Map strategyData = objectMapper.readValue(jsonResponse, new TypeReference<>() { + }); if (strategy.getStrategyData() != null) { strategy.getStrategyData().forEach(strategyData::putIfAbsent); @@ -118,7 +122,8 @@ public class MarketingStrategyV3Service { populateStrategyEntity(strategy, strategyData); repository.save(strategy); - addStatusHistoryEntry(strategy, "processing", "Создание фото и видео креативов. Это займет несколько минут..."); + addStatusHistoryEntry(strategy, "processing", + "Создание фото и видео креативов. Это займет несколько минут..."); repository.save(strategy); generateMediaAssets(strategy, analysis); @@ -158,8 +163,13 @@ public class MarketingStrategyV3Service { List> postCalendarList = new ArrayList<>(); List tasks = postingTaskService.getStrategyTasks(strategyId); - for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) { + // ИСПРАВЛЕНИЕ: Выдаем postIndex явно для фронтенда + List calendarItems = strategy.getPostCalendar(); + for (int i = 0; i < calendarItems.size(); i++) { + MarketingStrategy.PostCalendarItem item = calendarItems.get(i); Map dtoItem = new HashMap<>(); + + dtoItem.put("postIndex", i); // ЯВНЫЙ ИНДЕКС dtoItem.put("publishDate", item.getPublishDate()); dtoItem.put("platform", item.getPlatform()); dtoItem.put("contentType", item.getContentType()); @@ -244,34 +254,54 @@ public class MarketingStrategyV3Service { return item; } - public MarketingStrategy.PostCalendarItem regeneratePostVideo(String strategyId, int postIndex) { - Optional optStrategy = repository.findById(strategyId); - if (optStrategy.isEmpty()) { - return null; + // ИСПРАВЛЕНИЕ: Новый АСИНХРОННЫЙ метод для регенерации видео без таймаутов + @Async("reportGenerationExecutor") + public void regeneratePostVideoAsync(String strategyId, int postIndex) { + try { + Optional optStrategy = repository.findById(strategyId); + if (optStrategy.isEmpty()) { + log.error("Регенерация видео отменена: стратегия {} не найдена", strategyId); + return; + } + + MarketingStrategy strategy = optStrategy.get(); + List postCalendar = strategy.getPostCalendar(); + + if (postCalendar == null || postIndex < 0 || postIndex >= postCalendar.size()) { + log.error("Регенерация видео отменена: неверный postIndex {}", postIndex); + return; + } + + MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); + item.setContentType("видео"); + + // Ставим заглушку для фронтенда + item.setVideoUrl("generating..."); + item.setVideoFilename(null); + item.setImageUrl(null); + item.setImageFilename(null); + repository.save(strategy); + + Optional analysisOpt = analysisRepository.findById(strategy.getAnalysisId()); + if (analysisOpt.isEmpty()) { + log.error("Регенерация видео отменена: анализ не найден"); + return; + } + + MarketingAnalysisV3Document analysis = analysisOpt.get(); + String businessContext = getBusinessContext(analysis); + + log.info("Начинаем фоновую регенерацию видео для поста {} в стратегии {}", postIndex, strategyId); + + // Этот метод сам скачает, сохранит в MinIO и обновит поля item + doGenerateVideo(item, businessContext); + + repository.save(strategy); + log.info("Фоновая регенерация видео для поста {} успешно завершена!", postIndex); + + } catch (Exception e) { + log.error("Ошибка при фоновой регенерации видео: {}", e.getMessage(), e); } - - MarketingStrategy strategy = optStrategy.get(); - List postCalendar = strategy.getPostCalendar(); - - if (postCalendar == null || postIndex < 0 || postIndex >= postCalendar.size()) { - return null; - } - - MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex); - item.setContentType("видео"); - - Optional 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) { @@ -280,43 +310,78 @@ public class MarketingStrategyV3Service { if (req.getCustomerBehaviors() != null) { for (var cb : req.getCustomerBehaviors()) { String name = cb.name(); - if (name.contains("FAST") || name.contains("IMPULSE")) { conversion += 3; } - else if (name.contains("COMPARE")) { authority += 2; trust += 1; } - else if (name.contains("CONSULT")) { authority += 3; } - else if (name.contains("CASE") || name.contains("REVIEW")) { trust += 3; } + if (name.contains("FAST") || name.contains("IMPULSE")) { + conversion += 3; + } else if (name.contains("COMPARE")) { + authority += 2; + trust += 1; + } else if (name.contains("CONSULT")) { + authority += 3; + } else if (name.contains("CASE") || name.contains("REVIEW")) { + trust += 3; + } } } if (req.getAverageCheck() != null) { String check = req.getAverageCheck().name(); - if (check.contains("LOW")) { conversion += 2; } - else if (check.contains("MID")) { authority += 1; trust += 1; conversion += 1; } - else if (check.contains("HIGH")) { authority += 2; trust += 1; } + if (check.contains("LOW")) { + conversion += 2; + } else if (check.contains("MID")) { + authority += 1; + trust += 1; + conversion += 1; + } else if (check.contains("HIGH")) { + authority += 2; + trust += 1; + } } if (req.getBusinessStage() != null) { String stage = req.getBusinessStage().name(); - if (stage.contains("LAUNCH")) { entry += 3; } - else if (stage.contains("LESS")) { entry += 2; authority += 1; } - else { authority += 1; trust += 1; conversion += 1; } + if (stage.contains("LAUNCH")) { + entry += 3; + } else if (stage.contains("LESS")) { + entry += 2; + authority += 1; + } else { + authority += 1; + trust += 1; + conversion += 1; + } } if (req.getClientTarget() != null) { String target = req.getClientTarget().name(); - if (target.contains("B2B")) { authority += 2; trust += 1; } - else if (target.contains("B2C")) { trust += 1; conversion += 2; } - else { authority += 1; trust += 1; conversion += 1; } + if (target.contains("B2B")) { + authority += 2; + trust += 1; + } else if (target.contains("B2C")) { + trust += 1; + conversion += 2; + } else { + authority += 1; + trust += 1; + conversion += 1; + } } int max = Math.max(Math.max(entry, authority), Math.max(trust, conversion)); - if (max == authority && max == trust && req.getAverageCheck() != null && req.getAverageCheck().name().contains("HIGH")) return StrategyModel.AUTHORITY; - if (max == trust && max == conversion) return StrategyModel.TRUST; - if (max == entry && req.getBusinessStage() != null && req.getBusinessStage().name().contains("LAUNCH")) return StrategyModel.ENTRY; + if (max == authority && max == trust && req.getAverageCheck() != null + && req.getAverageCheck().name().contains("HIGH")) + return StrategyModel.AUTHORITY; + if (max == trust && max == conversion) + return StrategyModel.TRUST; + if (max == entry && req.getBusinessStage() != null && req.getBusinessStage().name().contains("LAUNCH")) + return StrategyModel.ENTRY; - if (max == conversion) return StrategyModel.CONVERSION; - if (max == trust) return StrategyModel.TRUST; - if (max == authority) return StrategyModel.AUTHORITY; + if (max == conversion) + return StrategyModel.CONVERSION; + if (max == trust) + return StrategyModel.TRUST; + if (max == authority) + return StrategyModel.AUTHORITY; return StrategyModel.ENTRY; } @@ -411,7 +476,8 @@ public class MarketingStrategyV3Service { doGenerateImage(item, businessContext, brandName, currentRefBytes); } - if (delayBetweenRequestsMs > 0) Thread.sleep(delayBetweenRequestsMs); + if (delayBetweenRequestsMs > 0) + Thread.sleep(delayBetweenRequestsMs); } catch (Exception e) { log.error("Error creating media for post '{}': {}", item.getTheme(), e.getMessage()); @@ -420,10 +486,15 @@ public class MarketingStrategyV3Service { } 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."; + String theme = item.getTheme() != null ? item.getTheme() : "Commercial content"; + String videoPrompt = String.format( + "High quality cinematic commercial video for %s. Scenario: %s. Photorealistic, 4k, professional motion, advertisement style.", + businessContext, theme); + try { - log.info("Starting video generation process for theme: {}", item.getTheme()); + log.info("VEO: Generating video. Prompt: '{}'", videoPrompt); 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"); @@ -431,20 +502,23 @@ public class MarketingStrategyV3Service { item.setVideoFilename(filename); item.setImageUrl(null); item.setImageFilename(null); - log.info("Video successfully generated and uploaded as {}", filename); + log.info("VEO: Video generated successfully: {}", filename); } else { - log.error("Video generation returned null for theme: {}", item.getTheme()); + log.error( + "VEO: Video generation returned null. Possible cause: extraction failed or API returned empty for prompt: '{}'", + videoPrompt); item.setVideoFilename(null); item.setVideoUrl(null); } } catch (Exception e) { - log.error("Error during doGenerateVideo execution: {}", e.getMessage(), e); + log.error("VEO: Critical error in video generation: {}", e.getMessage(), e); item.setVideoFilename(null); item.setVideoUrl(null); } } - 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); try { byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientRefBytes); @@ -468,8 +542,10 @@ public class MarketingStrategyV3Service { } private String getBusinessContext(MarketingAnalysisV3Document analysis) { - if (analysis == null || analysis.getRequestData() == null) return ""; - return analysis.getRequestData().getBusinessNiche() + ", product: " + analysis.getRequestData().getProductName(); + if (analysis == null || analysis.getRequestData() == null) + return ""; + return analysis.getRequestData().getBusinessNiche() + ", product: " + + analysis.getRequestData().getProductName(); } private String buildImagePrompt(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName) { @@ -502,24 +578,24 @@ public class MarketingStrategyV3Service { private String buildSystemPrompt(StrategyModel bestModel) { return """ - РОЛЬ: Ты — топовый Голливудский Креативный Директор и Chief Media Planner. - Бэкенд-система произвела расчет и выбрала доминирующую стратегию: %s. - - Твоя задача — проанализировать нишу и САМОСТОЯТЕЛЬНО: - 1. Выбрать наиболее эффективные платформы (от 1 до 3 шт). - 2. Определить оптимальную длительность стратегии (рекомендую 4 недели). - 3. Сгенерировать МАСШТАБНЫЙ контент-план. - - КРИТИЧЕСКОЕ ПРАВИЛО МАТЕМАТИКИ: - Ты ОБЯЗАН сгенерировать МИНИМУМ 3 поста на КАЖДУЮ неделю. - Если ты выбрал длительность 4 недели, в массиве 'postCalendar' должно быть РОВНО 12 постов (или больше). - Обязательно чередуй типы контента: 'фото' и 'видео'. - - ПРАВИЛА МОДЕЛИ: - %s - - ВЫВОДИ ТОЛЬКО ЧИСТЫЙ JSON! Никакого лишнего текста. - """.formatted(bestModel.getTitle(), bestModel.getContentRules()); + РОЛЬ: Ты — топовый Голливудский Креативный Директор и Chief Media Planner. + Бэкенд-система произвела расчет и выбрала доминирующую стратегию: %s. + + Твоя задача — проанализировать нишу и САМОСТОЯТЕЛЬНО: + 1. Выбрать наиболее эффективные платформы (от 1 до 3 шт). + 2. Определить оптимальную длительность стратегии (рекомендую 4 недели). + 3. Сгенерировать МАСШТАБНЫЙ контент-план. + + КРИТИЧЕСКОЕ ПРАВИЛО МАТЕМАТИКИ: + Ты ОБЯЗАН сгенерировать МИНИМУМ 3 поста на КАЖДУЮ неделю. + Если ты выбрал длительность 4 недели, в массиве 'postCalendar' должно быть РОВНО 12 постов (или больше). + Обязательно чередуй типы контента: 'фото' и 'видео'. + + ПРАВИЛА МОДЕЛИ: + %s + + ВЫВОДИ ТОЛЬКО ЧИСТЫЙ JSON! Никакого лишнего текста. + """.formatted(bestModel.getTitle(), bestModel.getContentRules()); } private String buildUserPrompt(MarketingAnalysisV3Document analysis, StrategyModel bestModel) throws Exception { @@ -527,57 +603,57 @@ public class MarketingStrategyV3Service { LocalDateTime startDate = LocalDateTime.now().plusDays(1); return """ - ДАННЫЕ БИЗНЕСА: - %s - - Старт публикаций: %s - ВЫБРАННАЯ МОДЕЛЬ: %s + ДАННЫЕ БИЗНЕСА: + %s - ВЫВЕДИ ТОЛЬКО JSON СТРОГО ПО ЭТОЙ СТРУКТУРЕ (СГЕНЕРИРУЙ 12 ПОСТОВ ЕСЛИ ВЫБРАЛ 4 НЕДЕЛИ): - { - "selectedModel": "%s", - "rationale": "Обоснование выбора платформ", - "recommendedDurationWeeks": 4, - "recommendedPlatforms": ["Instagram", "TikTok"], - "weeklyPlans": [ + Старт публикаций: %s + ВЫБРАННАЯ МОДЕЛЬ: %s + + ВЫВЕДИ ТОЛЬКО JSON СТРОГО ПО ЭТОЙ СТРУКТУРЕ (СГЕНЕРИРУЙ 12 ПОСТОВ ЕСЛИ ВЫБРАЛ 4 НЕДЕЛИ): { - "weekNumber": 1, - "mainThemes": ["Тема 1", "Тема 2", "Тема 3"], - "contentRecommendations": "Что снимать и писать", - "priorityPlatforms": ["Instagram"] + "selectedModel": "%s", + "rationale": "Обоснование выбора платформ", + "recommendedDurationWeeks": 4, + "recommendedPlatforms": ["Instagram", "TikTok"], + "weeklyPlans": [ + { + "weekNumber": 1, + "mainThemes": ["Тема 1", "Тема 2", "Тема 3"], + "contentRecommendations": "Что снимать и писать", + "priorityPlatforms": ["Instagram"] + } + ], + "postCalendar": [ + { + "publishDate": "2024-01-15T10:00:00", + "platform": "Instagram", + "contentType": "фото", + "theme": "Тема первого поста", + "postText": "Текст поста...", + "hashtags": ["#тег"], + "publishTime": "10:00" + }, + { + "publishDate": "2024-01-17T14:00:00", + "platform": "TikTok", + "contentType": "видео", + "theme": "Тема второго поста", + "postText": "Текст поста...", + "hashtags": ["#тег"], + "publishTime": "14:00" + } + ] } - ], - "postCalendar": [ - { - "publishDate": "2024-01-15T10:00:00", - "platform": "Instagram", - "contentType": "фото", - "theme": "Тема первого поста", - "postText": "Текст поста...", - "hashtags": ["#тег"], - "publishTime": "10:00" - }, - { - "publishDate": "2024-01-17T14:00:00", - "platform": "TikTok", - "contentType": "видео", - "theme": "Тема второго поста", - "postText": "Текст поста...", - "hashtags": ["#тег"], - "publishTime": "14:00" - } - ] - } - """.formatted( + """.formatted( clientDtoJson, startDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), bestModel.name(), - bestModel.name() - ); + bestModel.name()); } 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(); int startIdx = cleaned.indexOf("{"); @@ -590,7 +666,8 @@ public class MarketingStrategyV3Service { } private void addStatusHistoryEntry(MarketingStrategy strategy, String status, String message) { - if (strategy.getStatusHistory() == null) strategy.setStatusHistory(new ArrayList<>()); + if (strategy.getStatusHistory() == null) + strategy.setStatusHistory(new ArrayList<>()); strategy.getStatusHistory().add(new StatusHistoryEntry(status, LocalDateTime.now(), message)); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 69d8a28..50d2375 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -44,6 +44,8 @@ logging.level.com.mongodb=WARN logging.level.kz.konturai.parser.service.OllamaAnalyticsService=INFO logging.level.kz.konturai.parser.controller.MarketingController=DEBUG logging.level.kz.konturai.parser.service.MarketingAnalysisService=DEBUG +logging.level.kz.konturai.parser.service.GeminiVideoGenerationService=INFO +logging.level.kz.konturai.parser.service.NanoBananaImageGenerationService=INFO logging.level.kz.konturai.parser.config.MongoConfig=DEBUG logging.level.org.springframework.data.mongodb.core.convert=DEBUG