This commit is contained in:
arys
2026-03-05 02:40:30 +05:00
parent a2b6346e6d
commit 3936d67285
4 changed files with 292 additions and 197 deletions
@@ -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<String, Object> 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);
}
@@ -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<String, Object> parameters = new HashMap<>();
parameters.put("aspectRatio", "9:16");
Map<String, Object> 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<String, Object> initResponse = webClient.post()
.uri(URI.create(generateEndpoint))
@@ -123,86 +121,109 @@ public class GeminiVideoGenerationService {
.bodyValue(requestBody)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.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<String, Object> statusResponse = webClient.get()
.uri(URI.create(statusEndpoint))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.block(Duration.ofSeconds(30));
try {
Map<String, Object> statusResponse = webClient.get()
.uri(URI.create(fetchEndpoint))
.header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.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<String, Object> responseData = (Map<String, Object>) statusResponse.get("response");
return extractVideoBytes(responseData);
Map<String, Object> responseObj = (Map<String, Object>) 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<String, Object> responseData) {
if (responseData == null) return null;
private byte[] extractVideoBytes(Map<String, Object> data) {
if (data == null) return null;
try {
List<Map<String, Object>> predictions = (List<Map<String, Object>>) responseData.get("predictions");
if (predictions != null && !predictions.isEmpty()) {
Map<String, Object> 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;
}
@@ -64,7 +64,8 @@ public class MarketingStrategyV3Service {
return repository.findByUserIdOrderByCreatedAtDesc(userId);
}
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId, List<String> referenceFilenames) {
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId,
List<String> 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<String, Object> strategyData = objectMapper.readValue(jsonResponse, new TypeReference<>() {});
Map<String, Object> 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<Map<String, Object>> postCalendarList = new ArrayList<>();
List<PostingTask> tasks = postingTaskService.getStrategyTasks(strategyId);
for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
// ИСПРАВЛЕНИЕ: Выдаем postIndex явно для фронтенда
List<MarketingStrategy.PostCalendarItem> calendarItems = strategy.getPostCalendar();
for (int i = 0; i < calendarItems.size(); i++) {
MarketingStrategy.PostCalendarItem item = calendarItems.get(i);
Map<String, Object> 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<MarketingStrategy> optStrategy = repository.findById(strategyId);
if (optStrategy.isEmpty()) {
return null;
// ИСПРАВЛЕНИЕ: Новый АСИНХРОННЫЙ метод для регенерации видео без таймаутов
@Async("reportGenerationExecutor")
public void regeneratePostVideoAsync(String strategyId, int postIndex) {
try {
Optional<MarketingStrategy> optStrategy = repository.findById(strategyId);
if (optStrategy.isEmpty()) {
log.error("Регенерация видео отменена: стратегия {} не найдена", strategyId);
return;
}
MarketingStrategy strategy = optStrategy.get();
List<MarketingStrategy.PostCalendarItem> 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<MarketingAnalysisV3Document> 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<MarketingStrategy.PostCalendarItem> postCalendar = strategy.getPostCalendar();
if (postCalendar == null || postIndex < 0 || postIndex >= postCalendar.size()) {
return null;
}
MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex);
item.setContentType("видео");
Optional<MarketingAnalysisV3Document> 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));
}
@@ -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