diff --git a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java index f7aeb57..0257a56 100644 --- a/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/GeminiVideoGenerationService.java @@ -25,7 +25,9 @@ import java.util.*; public class GeminiVideoGenerationService { private static final Logger logger = LoggerFactory.getLogger(GeminiVideoGenerationService.class); - private static final String VERTEX_API_TEMPLATE = "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predict"; + + // ИЗМЕНЕНИЕ 1: Правильный эндпоинт для видео (PredictLongRunning) + private static final String VERTEX_API_TEMPLATE = "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predictLongRunning"; private static final String CREDENTIALS_FILE_PATH = "keys/google-key.json"; private final WebClient webClient; @@ -42,10 +44,10 @@ public class GeminiVideoGenerationService { public GeminiVideoGenerationService() { HttpClient httpClient = HttpClient.create() .resolver(DefaultAddressResolverGroup.INSTANCE) - .responseTimeout(Duration.ofMillis(600000)); // 10 минут таймаут + .responseTimeout(Duration.ofMillis(600000)); // 10 минут ExchangeStrategies strategies = ExchangeStrategies.builder() - .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(150 * 1024 * 1024)) // 150 MB под видео + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(150 * 1024 * 1024)) .build(); this.webClient = WebClient.builder() @@ -54,52 +56,8 @@ public class GeminiVideoGenerationService { .build(); } - // НОВЫЙ МЕТОД: Полная диагностика перед запуском - private boolean runPreflightCheck() { - logger.info("=== СТАРТ ДИАГНОСТИКИ VEO ==="); - - // Шаг 1. Проверяем переменные окружения - if (projectId == null || projectId.trim().isEmpty()) { - logger.error("[ПРОВАЛ] Project ID не задан в application.properties!"); - return false; - } - logger.info("[ОК] Project ID: {}", projectId); - logger.info("[ОК] Location: {}", location); - logger.info("[ОК] Model: {}", model); - - // Шаг 2. Проверяем файл ключа - ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH); - if (!resource.exists()) { - logger.error("[ПРОВАЛ] Файл ключа не найден по пути: src/main/resources/{}", CREDENTIALS_FILE_PATH); - return false; - } - logger.info("[ОК] Файл ключа найден."); - - // Шаг 3. Проверяем генерацию токена - String token = getAccessTokenFromResources(); - if (token == null || token.isEmpty()) { - logger.error("[ПРОВАЛ] Не удалось сгенерировать Access Token. Проверьте содержимое ключа и права аккаунта!"); - return false; - } - logger.info("[ОК] Access Token успешно сгенерирован (длина: {})", token.length()); - - // Шаг 4. Проверяем URL - String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); - logger.info("[ОК] Endpoint URL сформирован: {}", endpointUrl); - - logger.info("=== ДИАГНОСТИКА УСПЕШНО ЗАВЕРШЕНА ==="); - return true; - } - public byte[] generateVideo(String prompt) { - // Запускаем проверку перед каждой генерацией - if (!runPreflightCheck()) { - logger.error("VEO: Генерация отменена из-за провала базовых проверок."); - return null; - } - try { - String accessToken = getAccessTokenFromResources(); String endpointUrl = String.format(VERTEX_API_TEMPLATE, location, projectId, location, model); Map requestBody = new HashMap<>(); @@ -113,37 +71,63 @@ public class GeminiVideoGenerationService { parameters.put("aspectRatio", "9:16"); requestBody.put("parameters", parameters); - // Логируем сам запрос, чтобы проверить его формат - logger.info("VEO: Отправка JSON запроса в Google: {}", requestBody); + logger.info("VEO: Запускаем генерацию (Long Running Operation). Промпт: {}", prompt); - Map response = webClient.post() + // ШАГ 1: Запускаем задачу + Map initResponse = webClient.post() .uri(URI.create(endpointUrl)) - .header("Authorization", "Bearer " + accessToken) + .header("Authorization", "Bearer " + getAccessTokenFromResources()) .contentType(MediaType.APPLICATION_JSON) .bodyValue(requestBody) .retrieve() .bodyToMono(new ParameterizedTypeReference>() {}) - // ВНИМАНИЕ: МЫ ОТКЛЮЧИЛИ РЕТРАИ ДЛЯ ОТЛОВА ЧИСТОЙ ОШИБКИ ОТ GOOGLE - // .retryWhen(reactor.util.retry.Retry.backoff(3, Duration.ofSeconds(15)) - // .filter(t -> t instanceof WebClientResponseException && - // ((WebClientResponseException) t).getStatusCode().value() == 429)) - .block(Duration.ofMillis(600000)); + .block(Duration.ofSeconds(30)); - byte[] videoBytes = extractVideoFromResponse(response); - if (videoBytes != null) { - logger.info("VEO: Успех! Видео сгенерировано. Размер: {} байт", videoBytes.length); - } else { - logger.error("VEO: Видео не найдено в ответе. Ответ от Google: {}", response); + if (initResponse == null || !initResponse.containsKey("name")) { + logger.error("VEO: Ошибка! Не получен ID операции. Ответ: {}", initResponse); + return null; } - return videoBytes; + + String operationName = (String) initResponse.get("name"); + logger.info("VEO: Задача успешно принята Google. ID операции: {}", operationName); + + // ШАГ 2: Цикл опроса (Polling) + String operationUrl = String.format("https://%s-aiplatform.googleapis.com/v1/%s", location, operationName); + int attempts = 0; + int maxAttempts = 60; // Максимум 15 минут (60 попыток по 15 сек) + + while (attempts < maxAttempts) { + Thread.sleep(15000); // Ждем 15 секунд перед проверкой + attempts++; + logger.info("VEO: Проверка статуса видео... (попытка {}/{})", attempts, maxAttempts); + + Map statusResponse = webClient.get() + .uri(URI.create(operationUrl)) + .header("Authorization", "Bearer " + getAccessTokenFromResources()) // Обновляем токен + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .block(Duration.ofSeconds(30)); + + if (statusResponse != null) { + if (statusResponse.containsKey("error")) { + logger.error("VEO: Ошибка внутри рендеринга видео: {}", statusResponse.get("error")); + return null; + } + + Boolean isDone = (Boolean) statusResponse.get("done"); + if (Boolean.TRUE.equals(isDone)) { + logger.info("VEO: 🔥 Видео готово! Скачиваем результат..."); + Map responseObj = (Map) statusResponse.get("response"); + return extractVideoFromResponse(responseObj); + } + } + } + + logger.error("VEO: Превышено время ожидания рендеринга (таймаут)!"); + return null; } catch (WebClientResponseException e) { - // САМОЕ ВАЖНОЕ: Расшифровка ответа от Google - logger.error("=================================================="); - logger.error("VEO API ERROR CRASH!"); - logger.error("HTTP Status Code: {}", e.getStatusCode()); - logger.error("Error Body from Google: {}", e.getResponseBodyAsString()); - logger.error("=================================================="); + logger.error("VEO API ERROR! Status: {}, Body: {}", e.getStatusCode(), e.getResponseBodyAsString()); return null; } catch (Exception e) { logger.error("VEO API Неизвестная ошибка: {}", e.getMessage(), e); @@ -162,7 +146,7 @@ public class GeminiVideoGenerationService { return credentials.getAccessToken().getTokenValue(); } } catch (IOException e) { - logger.error("Ошибка при чтении файла ключа: {}", e.getMessage()); + logger.error("Ошибка ключа: {}", e.getMessage()); return null; } } @@ -174,21 +158,16 @@ public class GeminiVideoGenerationService { List> predictions = (List>) response.get("predictions"); if (predictions != null && !predictions.isEmpty()) { Map firstPrediction = predictions.get(0); - - // Проверяем разные варианты ключей, в которых Google может вернуть видео String base64Video = (String) firstPrediction.get("bytesBase64Encoded"); if (base64Video == null) { base64Video = (String) firstPrediction.get("b64"); } - if (base64Video != null) { return java.util.Base64.getDecoder().decode(base64Video); - } else { - logger.warn("VEO: Ключ с Base64 видео не найден в 'predictions'. Доступные ключи: {}", firstPrediction.keySet()); } } } catch (Exception e) { - logger.error("VEO: Ошибка парсинга ответа: {}", e.getMessage()); + logger.error("VEO: Ошибка извлечения Base64: {}", e.getMessage()); } return null; }