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