fix
This commit is contained in:
@@ -46,11 +46,10 @@ public class GeminiVideoGenerationService {
|
|||||||
public GeminiVideoGenerationService() {
|
public GeminiVideoGenerationService() {
|
||||||
HttpClient httpClient = HttpClient.create()
|
HttpClient httpClient = HttpClient.create()
|
||||||
.resolver(DefaultAddressResolverGroup.INSTANCE)
|
.resolver(DefaultAddressResolverGroup.INSTANCE)
|
||||||
.responseTimeout(Duration.ofMinutes(15));
|
.responseTimeout(Duration.ofMinutes(20));
|
||||||
|
|
||||||
// Лимит увеличен до 300МБ, чтобы 4K видео точно поместилось в памяти
|
|
||||||
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
||||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(300 * 1024 * 1024))
|
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(500 * 1024 * 1024)) // 500MB
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
this.webClient = WebClient.builder()
|
this.webClient = WebClient.builder()
|
||||||
@@ -67,30 +66,38 @@ public class GeminiVideoGenerationService {
|
|||||||
try (InputStream is = resource.getInputStream()) {
|
try (InputStream is = resource.getInputStream()) {
|
||||||
this.credentials = GoogleCredentials.fromStream(is)
|
this.credentials = GoogleCredentials.fromStream(is)
|
||||||
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"));
|
.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 {
|
} else {
|
||||||
logger.error("VEO: КРИТИЧЕСКАЯ ОШИБКА. Файл {} не найден!", CREDENTIALS_FILE_PATH);
|
logger.error("VEO: CRITICAL ERROR. File {} not found!", CREDENTIALS_FILE_PATH);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("VEO: Ошибка чтения ключа: {}", e.getMessage());
|
logger.error("VEO: Error reading credentials: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getValidAccessToken() {
|
private String getValidAccessToken() {
|
||||||
if (this.credentials == null) return null;
|
if (this.credentials == null) {
|
||||||
|
logger.error("VEO: Credentials not initialized!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
this.credentials.refreshIfExpired();
|
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) {
|
} catch (IOException e) {
|
||||||
logger.error("VEO: Ошибка обновления токена: {}", e.getMessage());
|
logger.error("VEO: Token refresh error: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] generateVideo(String prompt) {
|
public byte[] generateVideo(String prompt) {
|
||||||
if (projectId == null || projectId.trim().isEmpty()) {
|
if (projectId == null || projectId.trim().isEmpty()) {
|
||||||
logger.error("VEO: Не указан Project ID.");
|
logger.error("VEO: Project ID is not configured!");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,122 +105,286 @@ public class GeminiVideoGenerationService {
|
|||||||
if (accessToken == null) return null;
|
if (accessToken == null) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ШАГ 1: Запуск генерации
|
// ============================================================
|
||||||
|
// ШАГ 1: Запускаем генерацию через predictLongRunning
|
||||||
|
// ============================================================
|
||||||
String generateEndpoint = String.format(
|
String generateEndpoint = String.format(
|
||||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predictLongRunning",
|
"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<>();
|
Map<String, Object> parameters = new HashMap<>();
|
||||||
parameters.put("aspectRatio", "9:16");
|
parameters.put("aspectRatio", "9:16");
|
||||||
|
parameters.put("sampleCount", 1);
|
||||||
|
// Можно добавить: parameters.put("durationSeconds", 8);
|
||||||
|
|
||||||
Map<String, Object> requestBody = new HashMap<>();
|
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);
|
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()
|
Map<String, Object> initResponse;
|
||||||
.uri(URI.create(generateEndpoint))
|
try {
|
||||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
|
initResponse = webClient.post()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.uri(URI.create(generateEndpoint))
|
||||||
.bodyValue(requestBody)
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
|
||||||
.retrieve()
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
.bodyValue(requestBody)
|
||||||
.block(Duration.ofSeconds(120));
|
.retrieve()
|
||||||
|
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||||
if (initResponse == null || !initResponse.containsKey("name")) {
|
.block(Duration.ofSeconds(120));
|
||||||
logger.error("VEO: Ошибка: Google не вернул имя операции.");
|
} catch (WebClientResponseException e) {
|
||||||
|
logger.error("VEO: HTTP {} on initial request. Body: {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||||
return null;
|
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");
|
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 успел создать задачу в своей базе.
|
logger.info("VEO: Operation started. Name: {}", operationName);
|
||||||
// ЭТО УБЕРЕТ ОШИБКУ 404!
|
|
||||||
logger.info("VEO: Инициализация рендеринга... Ожидание 35 секунд перед проверкой статуса.");
|
|
||||||
Thread.sleep(35000);
|
|
||||||
|
|
||||||
// ШАГ 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 attempts = 0;
|
||||||
int maxAttempts = 80; // 20 минут максимум
|
int maxAttempts = 120; // 30 минут (120 * 15 сек)
|
||||||
|
|
||||||
while (attempts < maxAttempts) {
|
while (attempts < maxAttempts) {
|
||||||
|
Thread.sleep(15000); // 15 секунд между попытками
|
||||||
attempts++;
|
attempts++;
|
||||||
logger.info("VEO: Рендеринг видео... Проверка статуса ({} из {}). Пожалуйста, не выключайте сервер!", attempts, maxAttempts);
|
|
||||||
|
|
||||||
String currentToken = getValidAccessToken();
|
String currentToken = getValidAccessToken();
|
||||||
if (currentToken == null) continue;
|
if (currentToken == null) {
|
||||||
|
logger.warn("VEO: Could not get token on attempt {}, skipping...", attempts);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, Object> statusResponse = webClient.get()
|
Map<String, Object> statusResponse = webClient.post()
|
||||||
.uri(URI.create(fetchEndpoint))
|
.uri(URI.create(fetchEndpoint))
|
||||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken)
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(fetchBody)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||||
.block(Duration.ofSeconds(60));
|
.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")) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(statusResponse.get("done"))) {
|
// Проверяем done
|
||||||
logger.info("VEO: ВИДЕО УСПЕШНО СГЕНЕРИРОВАНО! Сохраняем файл...");
|
Object doneObj = statusResponse.get("done");
|
||||||
@SuppressWarnings("unchecked")
|
boolean isDone = Boolean.TRUE.equals(doneObj) || "true".equalsIgnoreCase(String.valueOf(doneObj));
|
||||||
Map<String, Object> responseObj = (Map<String, Object>) statusResponse.get("response");
|
|
||||||
return extractVideoBytes(responseObj);
|
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) {
|
} catch (WebClientResponseException e) {
|
||||||
// Глушим любые промежуточные ошибки связи, просто ждем дальше
|
logger.error("VEO: HTTP error on attempt {}: {} - {}",
|
||||||
logger.debug("VEO: Техническая задержка связи с Google ({}). Идем на следующий круг...", e.getStatusCode());
|
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;
|
return null;
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.error("VEO: Thread interrupted during polling");
|
||||||
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("VEO: Фатальная ошибка сервиса: {}", e.getMessage(), e);
|
logger.error("VEO: Fatal error: {}", e.getMessage(), e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] extractVideoBytes(Map<String, Object> data) {
|
/**
|
||||||
if (data == null) return null;
|
* Декодируем base64 в байты. Чистим строку от пробелов и data URI префиксов.
|
||||||
|
*/
|
||||||
|
private byte[] decodeBase64Video(String b64) {
|
||||||
try {
|
try {
|
||||||
String b64 = findFirstValue(data, "bytesBase64Encoded");
|
String clean = b64.replaceAll("\\s", "");
|
||||||
if (b64 != null && !b64.isEmpty()) {
|
if (clean.contains("base64,")) {
|
||||||
logger.info("VEO: Получены байты видео (размер кода: {}). Декодируем...", b64.length());
|
clean = clean.substring(clean.indexOf("base64,") + 7);
|
||||||
String cleanB64 = b64.replaceAll("\\s", "");
|
|
||||||
if (cleanB64.contains("base64,")) {
|
|
||||||
cleanB64 = cleanB64.substring(cleanB64.indexOf("base64,") + 7);
|
|
||||||
}
|
|
||||||
return Base64.getDecoder().decode(cleanB64);
|
|
||||||
}
|
}
|
||||||
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) {
|
} 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) {
|
private String findFirstValue(Object obj, String targetKey) {
|
||||||
if (obj instanceof Map) {
|
if (obj instanceof Map) {
|
||||||
Map<?, ?> map = (Map<?, ?>) obj;
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> map = (Map<String, Object>) obj;
|
||||||
if (map.containsKey(targetKey)) {
|
if (map.containsKey(targetKey)) {
|
||||||
Object val = map.get(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()) {
|
for (Object value : map.values()) {
|
||||||
String found = findFirstValue(value, targetKey);
|
String found = findFirstValue(value, targetKey);
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ public class MarketingStrategyV3Service {
|
|||||||
private final PostingTaskService postingTaskService;
|
private final PostingTaskService postingTaskService;
|
||||||
private final MinIOService minIOService;
|
private final MinIOService minIOService;
|
||||||
|
|
||||||
// Решение проблемы с @Async (Self-Injection)
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Lazy
|
@Lazy
|
||||||
private MarketingStrategyV3Service self;
|
private MarketingStrategyV3Service self;
|
||||||
@@ -65,7 +64,7 @@ public class MarketingStrategyV3Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId,
|
public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request, String userId,
|
||||||
List<String> referenceFilenames) {
|
List<String> referenceFilenames) {
|
||||||
MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId)
|
MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found"));
|
.orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found"));
|
||||||
|
|
||||||
@@ -85,7 +84,6 @@ public class MarketingStrategyV3Service {
|
|||||||
|
|
||||||
strategy = repository.save(strategy);
|
strategy = repository.save(strategy);
|
||||||
|
|
||||||
// Вызов через прокси для корректной работы @Async
|
|
||||||
self.processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
|
self.processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
|
||||||
|
|
||||||
return strategy;
|
return strategy;
|
||||||
@@ -93,7 +91,7 @@ public class MarketingStrategyV3Service {
|
|||||||
|
|
||||||
@Async("reportGenerationExecutor")
|
@Async("reportGenerationExecutor")
|
||||||
public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis,
|
public void processStrategyGenerationAsync(String strategyId, MarketingAnalysisV3Document analysis,
|
||||||
MarketingStrategy strategy) {
|
MarketingStrategy strategy) {
|
||||||
try {
|
try {
|
||||||
strategy.setStatus("processing");
|
strategy.setStatus("processing");
|
||||||
addStatusHistoryEntry(strategy, "processing", "Расчет баллов, генерация постов (минимум 3 в неделю)...");
|
addStatusHistoryEntry(strategy, "processing", "Расчет баллов, генерация постов (минимум 3 в неделю)...");
|
||||||
@@ -163,13 +161,12 @@ public class MarketingStrategyV3Service {
|
|||||||
List<Map<String, Object>> postCalendarList = new ArrayList<>();
|
List<Map<String, Object>> postCalendarList = new ArrayList<>();
|
||||||
List<PostingTask> tasks = postingTaskService.getStrategyTasks(strategyId);
|
List<PostingTask> tasks = postingTaskService.getStrategyTasks(strategyId);
|
||||||
|
|
||||||
// ИСПРАВЛЕНИЕ: Выдаем postIndex явно для фронтенда
|
|
||||||
List<MarketingStrategy.PostCalendarItem> calendarItems = strategy.getPostCalendar();
|
List<MarketingStrategy.PostCalendarItem> calendarItems = strategy.getPostCalendar();
|
||||||
for (int i = 0; i < calendarItems.size(); i++) {
|
for (int i = 0; i < calendarItems.size(); i++) {
|
||||||
MarketingStrategy.PostCalendarItem item = calendarItems.get(i);
|
MarketingStrategy.PostCalendarItem item = calendarItems.get(i);
|
||||||
Map<String, Object> dtoItem = new HashMap<>();
|
Map<String, Object> dtoItem = new HashMap<>();
|
||||||
|
|
||||||
dtoItem.put("postIndex", i); // ЯВНЫЙ ИНДЕКС
|
dtoItem.put("postIndex", i);
|
||||||
dtoItem.put("publishDate", item.getPublishDate());
|
dtoItem.put("publishDate", item.getPublishDate());
|
||||||
dtoItem.put("platform", item.getPlatform());
|
dtoItem.put("platform", item.getPlatform());
|
||||||
dtoItem.put("contentType", item.getContentType());
|
dtoItem.put("contentType", item.getContentType());
|
||||||
@@ -254,7 +251,6 @@ public class MarketingStrategyV3Service {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ИСПРАВЛЕНИЕ: Новый АСИНХРОННЫЙ метод для регенерации видео без таймаутов
|
|
||||||
@Async("reportGenerationExecutor")
|
@Async("reportGenerationExecutor")
|
||||||
public void regeneratePostVideoAsync(String strategyId, int postIndex) {
|
public void regeneratePostVideoAsync(String strategyId, int postIndex) {
|
||||||
try {
|
try {
|
||||||
@@ -275,7 +271,6 @@ public class MarketingStrategyV3Service {
|
|||||||
MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex);
|
MarketingStrategy.PostCalendarItem item = postCalendar.get(postIndex);
|
||||||
item.setContentType("видео");
|
item.setContentType("видео");
|
||||||
|
|
||||||
// Ставим заглушку для фронтенда
|
|
||||||
item.setVideoUrl("generating...");
|
item.setVideoUrl("generating...");
|
||||||
item.setVideoFilename(null);
|
item.setVideoFilename(null);
|
||||||
item.setImageUrl(null);
|
item.setImageUrl(null);
|
||||||
@@ -293,7 +288,6 @@ public class MarketingStrategyV3Service {
|
|||||||
|
|
||||||
log.info("Начинаем фоновую регенерацию видео для поста {} в стратегии {}", postIndex, strategyId);
|
log.info("Начинаем фоновую регенерацию видео для поста {} в стратегии {}", postIndex, strategyId);
|
||||||
|
|
||||||
// Этот метод сам скачает, сохранит в MinIO и обновит поля item
|
|
||||||
doGenerateVideo(item, businessContext);
|
doGenerateVideo(item, businessContext);
|
||||||
|
|
||||||
repository.save(strategy);
|
repository.save(strategy);
|
||||||
@@ -485,10 +479,12 @@ public class MarketingStrategyV3Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === ВОТ ЗДЕСЬ ОБНОВЛЕННЫЙ ПРОМПТ ДЛЯ ВИДЕО ===
|
||||||
private void doGenerateVideo(MarketingStrategy.PostCalendarItem item, String businessContext) {
|
private void doGenerateVideo(MarketingStrategy.PostCalendarItem item, String businessContext) {
|
||||||
String theme = item.getTheme() != null ? item.getTheme() : "Commercial content";
|
String theme = item.getTheme() != null ? item.getTheme() : "Commercial content";
|
||||||
|
// Изменили промпт: убрали 4K, добавили стиль TikTok/Instagram Reels, сделали его более оптимизированным
|
||||||
String videoPrompt = String.format(
|
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);
|
businessContext, theme);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -516,9 +512,10 @@ public class MarketingStrategyV3Service {
|
|||||||
item.setVideoUrl(null);
|
item.setVideoUrl(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ===============================================
|
||||||
|
|
||||||
private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName,
|
private void doGenerateImage(MarketingStrategy.PostCalendarItem item, String businessContext, String brandName,
|
||||||
byte[] clientRefBytes) {
|
byte[] clientRefBytes) {
|
||||||
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
|
String imagePrompt = buildImagePrompt(item, businessContext, brandName);
|
||||||
try {
|
try {
|
||||||
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientRefBytes);
|
byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, clientRefBytes);
|
||||||
|
|||||||
Reference in New Issue
Block a user