fix
This commit is contained in:
@@ -46,11 +46,10 @@ public class GeminiVideoGenerationService {
|
||||
public GeminiVideoGenerationService() {
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.resolver(DefaultAddressResolverGroup.INSTANCE)
|
||||
.responseTimeout(Duration.ofMinutes(15));
|
||||
.responseTimeout(Duration.ofMinutes(20));
|
||||
|
||||
// Лимит увеличен до 300МБ, чтобы 4K видео точно поместилось в памяти
|
||||
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(300 * 1024 * 1024))
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(500 * 1024 * 1024)) // 500MB
|
||||
.build();
|
||||
|
||||
this.webClient = WebClient.builder()
|
||||
@@ -67,30 +66,38 @@ public class GeminiVideoGenerationService {
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
this.credentials = GoogleCredentials.fromStream(is)
|
||||
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"));
|
||||
logger.info("VEO: Учетные данные загружены. Модель={}", model);
|
||||
logger.info("VEO: Credentials loaded. Project={}, Location={}, Model={}", projectId, location, model);
|
||||
}
|
||||
} else {
|
||||
logger.error("VEO: КРИТИЧЕСКАЯ ОШИБКА. Файл {} не найден!", CREDENTIALS_FILE_PATH);
|
||||
logger.error("VEO: CRITICAL ERROR. File {} not found!", CREDENTIALS_FILE_PATH);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("VEO: Ошибка чтения ключа: {}", e.getMessage());
|
||||
logger.error("VEO: Error reading credentials: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getValidAccessToken() {
|
||||
if (this.credentials == null) return null;
|
||||
if (this.credentials == null) {
|
||||
logger.error("VEO: Credentials not initialized!");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
this.credentials.refreshIfExpired();
|
||||
return this.credentials.getAccessToken().getTokenValue();
|
||||
String token = this.credentials.getAccessToken().getTokenValue();
|
||||
if (token == null || token.isEmpty()) {
|
||||
logger.error("VEO: Access token is null/empty after refresh!");
|
||||
return null;
|
||||
}
|
||||
return token;
|
||||
} catch (IOException e) {
|
||||
logger.error("VEO: Ошибка обновления токена: {}", e.getMessage());
|
||||
logger.error("VEO: Token refresh error: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] generateVideo(String prompt) {
|
||||
if (projectId == null || projectId.trim().isEmpty()) {
|
||||
logger.error("VEO: Не указан Project ID.");
|
||||
logger.error("VEO: Project ID is not configured!");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -98,122 +105,286 @@ public class GeminiVideoGenerationService {
|
||||
if (accessToken == null) return null;
|
||||
|
||||
try {
|
||||
// ШАГ 1: Запуск генерации
|
||||
// ============================================================
|
||||
// ШАГ 1: Запускаем генерацию через predictLongRunning
|
||||
// ============================================================
|
||||
String generateEndpoint = String.format(
|
||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predictLongRunning",
|
||||
location, projectId, location, model
|
||||
);
|
||||
location, projectId, location, model);
|
||||
|
||||
// Структура запроса для Veo 2.0
|
||||
Map<String, Object> instance = new HashMap<>();
|
||||
instance.put("prompt", prompt);
|
||||
|
||||
// Оставляем ТОЛЬКО формат видео. Лишние параметры крашат Google API.
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("aspectRatio", "9:16");
|
||||
parameters.put("sampleCount", 1);
|
||||
// Можно добавить: parameters.put("durationSeconds", 8);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("instances", Collections.singletonList(Collections.singletonMap("prompt", prompt)));
|
||||
requestBody.put("instances", Collections.singletonList(instance));
|
||||
requestBody.put("parameters", parameters);
|
||||
|
||||
logger.info("VEO: Отправка промпта в нейросеть: '{}'", prompt);
|
||||
logger.info("VEO: Starting generation. Endpoint: {}", generateEndpoint);
|
||||
logger.info("VEO: Prompt: '{}'", prompt);
|
||||
|
||||
Map<String, Object> initResponse = webClient.post()
|
||||
.uri(URI.create(generateEndpoint))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.block(Duration.ofSeconds(120));
|
||||
|
||||
if (initResponse == null || !initResponse.containsKey("name")) {
|
||||
logger.error("VEO: Ошибка: Google не вернул имя операции.");
|
||||
Map<String, Object> initResponse;
|
||||
try {
|
||||
initResponse = webClient.post()
|
||||
.uri(URI.create(generateEndpoint))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.block(Duration.ofSeconds(120));
|
||||
} catch (WebClientResponseException e) {
|
||||
logger.error("VEO: HTTP {} on initial request. Body: {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (initResponse == null) {
|
||||
logger.error("VEO: Initial response is null!");
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("VEO: Initial response: {}", objectMapper.writeValueAsString(initResponse));
|
||||
|
||||
// Получаем имя операции
|
||||
String operationName = (String) initResponse.get("name");
|
||||
logger.info("VEO: Задача поставлена в очередь видеокарт Google.");
|
||||
if (operationName == null || operationName.isEmpty()) {
|
||||
logger.error("VEO: No 'name' field in response! Full response: {}", objectMapper.writeValueAsString(initResponse));
|
||||
return null;
|
||||
}
|
||||
|
||||
// ВАЖНО: Ждем 35 секунд перед первой проверкой, чтобы Google успел создать задачу в своей базе.
|
||||
// ЭТО УБЕРЕТ ОШИБКУ 404!
|
||||
logger.info("VEO: Инициализация рендеринга... Ожидание 35 секунд перед проверкой статуса.");
|
||||
Thread.sleep(35000);
|
||||
logger.info("VEO: Operation started. Name: {}", operationName);
|
||||
|
||||
// ШАГ 2: Мягкий опрос статуса
|
||||
String fetchEndpoint = String.format("https://%s-aiplatform.googleapis.com/v1/%s", location, operationName);
|
||||
// ============================================================
|
||||
// ШАГ 2: Polling через fetchPredictOperation
|
||||
// ============================================================
|
||||
String fetchEndpoint = String.format(
|
||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:fetchPredictOperation",
|
||||
location, projectId, location, model);
|
||||
|
||||
Map<String, Object> fetchBody = new HashMap<>();
|
||||
fetchBody.put("operationName", operationName);
|
||||
|
||||
int attempts = 0;
|
||||
int maxAttempts = 80; // 20 минут максимум
|
||||
int maxAttempts = 120; // 30 минут (120 * 15 сек)
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
Thread.sleep(15000); // 15 секунд между попытками
|
||||
attempts++;
|
||||
logger.info("VEO: Рендеринг видео... Проверка статуса ({} из {}). Пожалуйста, не выключайте сервер!", attempts, maxAttempts);
|
||||
|
||||
String currentToken = getValidAccessToken();
|
||||
if (currentToken == null) continue;
|
||||
if (currentToken == null) {
|
||||
logger.warn("VEO: Could not get token on attempt {}, skipping...", attempts);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> statusResponse = webClient.get()
|
||||
Map<String, Object> statusResponse = webClient.post()
|
||||
.uri(URI.create(fetchEndpoint))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(fetchBody)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.block(Duration.ofSeconds(60));
|
||||
|
||||
if (statusResponse == null) continue;
|
||||
if (statusResponse == null) {
|
||||
logger.warn("VEO: Null status response on attempt {}", attempts);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Всегда логируем сырой ответ для диагностики
|
||||
String rawJson = objectMapper.writeValueAsString(statusResponse);
|
||||
logger.info("VEO: [Attempt {}/{}] Raw response: {}", attempts, maxAttempts, rawJson);
|
||||
|
||||
// Проверяем ошибку в ответе
|
||||
if (statusResponse.containsKey("error")) {
|
||||
logger.error("VEO: ОШИБКА ВНУТРИ GOOGLE: {}", objectMapper.writeValueAsString(statusResponse.get("error")));
|
||||
Object errorObj = statusResponse.get("error");
|
||||
logger.error("VEO: Google returned error: {}", objectMapper.writeValueAsString(errorObj));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(statusResponse.get("done"))) {
|
||||
logger.info("VEO: ВИДЕО УСПЕШНО СГЕНЕРИРОВАНО! Сохраняем файл...");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> responseObj = (Map<String, Object>) statusResponse.get("response");
|
||||
return extractVideoBytes(responseObj);
|
||||
// Проверяем done
|
||||
Object doneObj = statusResponse.get("done");
|
||||
boolean isDone = Boolean.TRUE.equals(doneObj) || "true".equalsIgnoreCase(String.valueOf(doneObj));
|
||||
|
||||
if (!isDone) {
|
||||
logger.info("VEO: Still processing... attempt {}/{}", attempts, maxAttempts);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info("VEO: Operation DONE on attempt {}! Extracting video...", attempts);
|
||||
|
||||
// ============================================================
|
||||
// ШАГ 3: Извлекаем видео из ответа
|
||||
// ============================================================
|
||||
// Структура ответа Veo:
|
||||
// {
|
||||
// "done": true,
|
||||
// "response": {
|
||||
// "@type": "...",
|
||||
// "predictions": [
|
||||
// {
|
||||
// "bytesBase64Encoded": "...", <- вариант 1
|
||||
// "videoUri": "gs://...", <- вариант 2
|
||||
// "mimeType": "video/mp4"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
|
||||
// Достаем объект response
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> responseObj = (Map<String, Object>) statusResponse.get("response");
|
||||
if (responseObj == null) {
|
||||
// Иногда данные могут быть прямо в корне при done=true
|
||||
logger.warn("VEO: No 'response' field, trying root object...");
|
||||
responseObj = statusResponse;
|
||||
}
|
||||
|
||||
// Пробуем извлечь base64
|
||||
String b64 = findFirstValue(responseObj, "bytesBase64Encoded");
|
||||
if (b64 != null && !b64.isEmpty()) {
|
||||
logger.info("VEO: Found base64 video (length={})", b64.length());
|
||||
return decodeBase64Video(b64);
|
||||
}
|
||||
|
||||
// Пробуем GCS URI
|
||||
String videoUri = findFirstValue(responseObj, "videoUri");
|
||||
if (videoUri == null) {
|
||||
videoUri = findFirstValue(responseObj, "uri");
|
||||
}
|
||||
if (videoUri != null && !videoUri.isEmpty()) {
|
||||
logger.info("VEO: Found video URI: {}", videoUri);
|
||||
return downloadFromGcs(videoUri, currentToken);
|
||||
}
|
||||
|
||||
// Пробуем найти в predictions напрямую
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> predictions = (List<Map<String, Object>>) responseObj.get("predictions");
|
||||
if (predictions != null && !predictions.isEmpty()) {
|
||||
Map<String, Object> firstPrediction = predictions.get(0);
|
||||
logger.info("VEO: First prediction keys: {}", firstPrediction.keySet());
|
||||
|
||||
b64 = (String) firstPrediction.get("bytesBase64Encoded");
|
||||
if (b64 != null && !b64.isEmpty()) {
|
||||
return decodeBase64Video(b64);
|
||||
}
|
||||
|
||||
String uri = (String) firstPrediction.get("videoUri");
|
||||
if (uri == null) uri = (String) firstPrediction.get("uri");
|
||||
if (uri != null && !uri.isEmpty()) {
|
||||
return downloadFromGcs(uri, currentToken);
|
||||
}
|
||||
}
|
||||
|
||||
logger.error("VEO: Video not found in response! Full dump: {}", rawJson);
|
||||
return null;
|
||||
|
||||
} catch (WebClientResponseException e) {
|
||||
// Глушим любые промежуточные ошибки связи, просто ждем дальше
|
||||
logger.debug("VEO: Техническая задержка связи с Google ({}). Идем на следующий круг...", e.getStatusCode());
|
||||
logger.error("VEO: HTTP error on attempt {}: {} - {}",
|
||||
attempts, e.getStatusCode(), e.getResponseBodyAsString());
|
||||
if (e.getStatusCode().is4xxClientError()) {
|
||||
logger.error("VEO: 4xx error - stopping poll to avoid wasting time.");
|
||||
return null;
|
||||
}
|
||||
// При 5xx продолжаем
|
||||
}
|
||||
|
||||
// Ждем 15 секунд перед следующей проверкой
|
||||
Thread.sleep(15000);
|
||||
}
|
||||
|
||||
logger.error("VEO: Превышено время ожидания рендеринга (20 минут).");
|
||||
logger.error("VEO: TIMEOUT! Waited {} minutes, Google did not complete.", (maxAttempts * 15 / 60));
|
||||
return null;
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
logger.error("VEO: Thread interrupted during polling");
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
logger.error("VEO: Фатальная ошибка сервиса: {}", e.getMessage(), e);
|
||||
logger.error("VEO: Fatal error: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] extractVideoBytes(Map<String, Object> data) {
|
||||
if (data == null) return null;
|
||||
/**
|
||||
* Декодируем base64 в байты. Чистим строку от пробелов и data URI префиксов.
|
||||
*/
|
||||
private byte[] decodeBase64Video(String b64) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
return Base64.getDecoder().decode(cleanB64);
|
||||
String clean = b64.replaceAll("\\s", "");
|
||||
if (clean.contains("base64,")) {
|
||||
clean = clean.substring(clean.indexOf("base64,") + 7);
|
||||
}
|
||||
logger.error("VEO: Видео не найдено в ответе! Дамп: {}", objectMapper.writeValueAsString(data));
|
||||
byte[] bytes = Base64.getDecoder().decode(clean);
|
||||
logger.info("VEO: Successfully decoded {} bytes from base64", bytes.length);
|
||||
return bytes;
|
||||
} catch (Exception e) {
|
||||
logger.error("VEO: Ошибка извлечения байтов: {}", e.getMessage());
|
||||
logger.error("VEO: Failed to decode base64: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Скачиваем видео с GCS URI.
|
||||
*
|
||||
* Два варианта URI:
|
||||
* 1. gs://bucket/path — нужен Storage API
|
||||
* 2. https://storage.googleapis.com/bucket/path — прямой HTTP
|
||||
*/
|
||||
private byte[] downloadFromGcs(String uri, String accessToken) {
|
||||
try {
|
||||
String downloadUrl;
|
||||
|
||||
if (uri.startsWith("gs://")) {
|
||||
// Конвертируем gs:// в HTTPS URL
|
||||
// gs://bucket-name/path/to/file.mp4
|
||||
// -> https://storage.googleapis.com/bucket-name/path/to/file.mp4
|
||||
String withoutScheme = uri.substring(5); // убираем "gs://"
|
||||
downloadUrl = "https://storage.googleapis.com/" + withoutScheme;
|
||||
logger.info("VEO: Converting GCS URI to HTTPS: {}", downloadUrl);
|
||||
} else if (uri.startsWith("https://")) {
|
||||
downloadUrl = uri;
|
||||
} else {
|
||||
logger.error("VEO: Unknown URI format: {}", uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("VEO: Downloading video from: {}", downloadUrl);
|
||||
|
||||
byte[] videoBytes = webClient.get()
|
||||
.uri(URI.create(downloadUrl))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
|
||||
.retrieve()
|
||||
.bodyToMono(byte[].class)
|
||||
.block(Duration.ofMinutes(5));
|
||||
|
||||
if (videoBytes != null && videoBytes.length > 0) {
|
||||
logger.info("VEO: Downloaded {} bytes from GCS", videoBytes.length);
|
||||
return videoBytes;
|
||||
} else {
|
||||
logger.error("VEO: Downloaded empty bytes from GCS URI: {}", uri);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("VEO: Error downloading from GCS {}: {}", uri, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Рекурсивный поиск значения по ключу в вложенных Map/List структурах.
|
||||
*/
|
||||
private String findFirstValue(Object obj, String targetKey) {
|
||||
if (obj instanceof Map) {
|
||||
Map<?, ?> map = (Map<?, ?>) obj;
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) obj;
|
||||
if (map.containsKey(targetKey)) {
|
||||
Object val = map.get(targetKey);
|
||||
if (val instanceof String) return (String) val;
|
||||
if (val instanceof String s && !s.isEmpty()) return s;
|
||||
}
|
||||
for (Object value : map.values()) {
|
||||
String found = findFirstValue(value, targetKey);
|
||||
|
||||
@@ -38,7 +38,6 @@ public class MarketingStrategyV3Service {
|
||||
private final PostingTaskService postingTaskService;
|
||||
private final MinIOService minIOService;
|
||||
|
||||
// Решение проблемы с @Async (Self-Injection)
|
||||
@Autowired
|
||||
@Lazy
|
||||
private MarketingStrategyV3Service self;
|
||||
@@ -65,7 +64,7 @@ public class MarketingStrategyV3Service {
|
||||
}
|
||||
|
||||
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId,
|
||||
List<String> referenceFilenames) {
|
||||
List<String> referenceFilenames) {
|
||||
MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found"));
|
||||
|
||||
@@ -85,7 +84,6 @@ public class MarketingStrategyV3Service {
|
||||
|
||||
strategy = repository.save(strategy);
|
||||
|
||||
// Вызов через прокси для корректной работы @Async
|
||||
self.processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
|
||||
|
||||
return strategy;
|
||||
@@ -93,7 +91,7 @@ public class MarketingStrategyV3Service {
|
||||
|
||||
@Async("reportGenerationExecutor")
|
||||
public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis,
|
||||
MarketingStrategy strategy) {
|
||||
MarketingStrategy strategy) {
|
||||
try {
|
||||
strategy.setStatus("processing");
|
||||
addStatusHistoryEntry(strategy, "processing", "Расчет баллов, генерация постов (минимум 3 в неделю)...");
|
||||
@@ -163,13 +161,12 @@ public class MarketingStrategyV3Service {
|
||||
List<Map<String, Object>> postCalendarList = new ArrayList<>();
|
||||
List<PostingTask> tasks = postingTaskService.getStrategyTasks(strategyId);
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Выдаем 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("postIndex", i);
|
||||
dtoItem.put("publishDate", item.getPublishDate());
|
||||
dtoItem.put("platform", item.getPlatform());
|
||||
dtoItem.put("contentType", item.getContentType());
|
||||
@@ -254,7 +251,6 @@ public class MarketingStrategyV3Service {
|
||||
return item;
|
||||
}
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Новый АСИНХРОННЫЙ метод для регенерации видео без таймаутов
|
||||
@Async("reportGenerationExecutor")
|
||||
public void regeneratePostVideoAsync(String strategyId, int postIndex) {
|
||||
try {
|
||||
@@ -275,7 +271,6 @@ public class MarketingStrategyV3Service {
|
||||
MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex);
|
||||
item.setContentType("видео");
|
||||
|
||||
// Ставим заглушку для фронтенда
|
||||
item.setVideoUrl("generating...");
|
||||
item.setVideoFilename(null);
|
||||
item.setImageUrl(null);
|
||||
@@ -293,7 +288,6 @@ public class MarketingStrategyV3Service {
|
||||
|
||||
log.info("Начинаем фоновую регенерацию видео для поста {} в стратегии {}", postIndex, strategyId);
|
||||
|
||||
// Этот метод сам скачает, сохранит в MinIO и обновит поля item
|
||||
doGenerateVideo(item, businessContext);
|
||||
|
||||
repository.save(strategy);
|
||||
@@ -485,10 +479,12 @@ public class MarketingStrategyV3Service {
|
||||
}
|
||||
}
|
||||
|
||||
// === ВОТ ЗДЕСЬ ОБНОВЛЕННЫЙ ПРОМПТ ДЛЯ ВИДЕО ===
|
||||
private void doGenerateVideo(MarketingStrategy.PostCalendarItem item, String businessContext) {
|
||||
String theme = item.getTheme() != null ? item.getTheme() : "Commercial content";
|
||||
// Изменили промпт: убрали 4K, добавили стиль TikTok/Instagram Reels, сделали его более оптимизированным
|
||||
String videoPrompt = String.format(
|
||||
"High quality cinematic commercial video for %s. Scenario: %s. Photorealistic, 4k, professional motion, advertisement style.",
|
||||
"High quality engaging vertical video for %s. Scenario: %s. Photorealistic, bright lighting, trendy TikTok and Instagram Reels style, dynamic motion.",
|
||||
businessContext, theme);
|
||||
|
||||
try {
|
||||
@@ -516,9 +512,10 @@ public class MarketingStrategyV3Service {
|
||||
item.setVideoUrl(null);
|
||||
}
|
||||
}
|
||||
// ===============================================
|
||||
|
||||
private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName,
|
||||
byte[] clientRefBytes) {
|
||||
byte[] clientRefBytes) {
|
||||
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
|
||||
try {
|
||||
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientRefBytes);
|
||||
|
||||
Reference in New Issue
Block a user