fix
This commit is contained in:
@@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||
@@ -15,6 +16,8 @@ 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;
|
||||
import java.net.URI;
|
||||
@@ -29,12 +32,16 @@ public class GeminiVideoGenerationService {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
// Кэшируем credentials, чтобы не дергать диск и Google API каждые 15 секунд
|
||||
private GoogleCredentials credentials;
|
||||
|
||||
@Value("${google.cloud.project-id}")
|
||||
private String projectId;
|
||||
|
||||
@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;
|
||||
|
||||
@@ -44,7 +51,7 @@ public class GeminiVideoGenerationService {
|
||||
.responseTimeout(Duration.ofMinutes(15)); // Долгий таймаут для видео
|
||||
|
||||
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(200 * 1024 * 1024)) // 200MB лимит для больших видео
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(200 * 1024 * 1024)) // 200MB
|
||||
.build();
|
||||
|
||||
this.webClient = WebClient.builder()
|
||||
@@ -53,19 +60,50 @@ public class GeminiVideoGenerationService {
|
||||
.build();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void initCredentials() {
|
||||
try {
|
||||
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 успешно загружены.");
|
||||
}
|
||||
} else {
|
||||
logger.error("VEO: КРИТИЧЕСКАЯ ОШИБКА. Файл ключа не найден по пути: {}", CREDENTIALS_FILE_PATH);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("VEO: Ошибка парсинга ключа Google: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getValidAccessToken() {
|
||||
if (this.credentials == null) {
|
||||
logger.error("VEO: Учетные данные не инициализированы. Проверьте google-key.json");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Если токен истек, он обновится. Если нет — отдаст текущий. Никакого спама в API.
|
||||
this.credentials.refreshIfExpired();
|
||||
return this.credentials.getAccessToken().getTokenValue();
|
||||
} catch (IOException e) {
|
||||
logger.error("VEO: Ошибка получения/обновления токена: {}", 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 (google.cloud.project-id).");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String accessToken = getAccessTokenFromResources();
|
||||
if (accessToken == null) {
|
||||
logger.error("VEO: Ошибка токена доступа.");
|
||||
return null;
|
||||
}
|
||||
String accessToken = getValidAccessToken();
|
||||
if (accessToken == null) return null;
|
||||
|
||||
try {
|
||||
// ШАГ 1: Отправляем запрос на генерацию
|
||||
String generateEndpoint = String.format(
|
||||
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predictLongRunning",
|
||||
@@ -73,21 +111,14 @@ public class GeminiVideoGenerationService {
|
||||
);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
List<Map<String, Object>> instances = new ArrayList<>();
|
||||
Map<String, Object> instance = new HashMap<>();
|
||||
instance.put("prompt", prompt);
|
||||
instances.add(instance);
|
||||
requestBody.put("instances", instances);
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("aspectRatio", "9:16");
|
||||
requestBody.put("parameters", parameters);
|
||||
requestBody.put("instances", Collections.singletonList(Collections.singletonMap("prompt", prompt)));
|
||||
requestBody.put("parameters", Collections.singletonMap("aspectRatio", "9:16"));
|
||||
|
||||
logger.info("VEO: Запускаем генерацию видео. Промпт: '{}'", prompt);
|
||||
|
||||
Map<String, Object> initResponse = webClient.post()
|
||||
.uri(URI.create(generateEndpoint))
|
||||
.header("Authorization", "Bearer " + accessToken)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
@@ -95,40 +126,41 @@ public class GeminiVideoGenerationService {
|
||||
.block(Duration.ofSeconds(30));
|
||||
|
||||
if (initResponse == null || !initResponse.containsKey("name")) {
|
||||
logger.error("VEO: Не удалось получить имя операции от Google. Ответ: {}", initResponse);
|
||||
logger.error("VEO: Сервер Google не вернул operation name. Ответ: {}", initResponse);
|
||||
return null;
|
||||
}
|
||||
|
||||
String operationName = (String) initResponse.get("name");
|
||||
logger.info("VEO: Задача принята сервером Google. ID операции: {}", operationName);
|
||||
logger.info("VEO: Задача успешно принята. ID операции: {}", operationName);
|
||||
|
||||
// ШАГ 2: Опрос статуса задачи каждые 15 секунд (до 15 минут)
|
||||
// ИСПОЛЬЗУЕМ v1 ДЛЯ ОПРОСА СТАТУСА (Google починил этот эндпоинт)
|
||||
// ШАГ 2: Опрос статуса задачи
|
||||
String statusEndpoint = String.format("https://%s-aiplatform.googleapis.com/v1/%s", location, operationName);
|
||||
int attempts = 0;
|
||||
int maxAttempts = 60; // 60 * 15 сек = 15 минут
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
Thread.sleep(15000);
|
||||
Thread.sleep(15000); // Ожидание 15 секунд
|
||||
attempts++;
|
||||
logger.info("VEO: Проверка статуса ({} / {})...", attempts, maxAttempts);
|
||||
logger.info("VEO: Проверка статуса (попытка {} из {})...", attempts, maxAttempts);
|
||||
|
||||
// Берем актуальный токен (обновится сам, если прошло > 1 часа)
|
||||
String currentToken = getValidAccessToken();
|
||||
|
||||
Map<String, Object> statusResponse = webClient.get()
|
||||
.uri(URI.create(statusEndpoint))
|
||||
.header("Authorization", "Bearer " + getAccessTokenFromResources()) // Токен нужно обновлять
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + currentToken)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.block(Duration.ofSeconds(30));
|
||||
|
||||
if (statusResponse != null) {
|
||||
if (statusResponse.containsKey("error")) {
|
||||
logger.error("VEO: Внутренняя ошибка генерации Google: {}", statusResponse.get("error"));
|
||||
logger.error("VEO: Ошибка внутри процесса генерации: {}", statusResponse.get("error"));
|
||||
return null;
|
||||
}
|
||||
|
||||
Boolean isDone = (Boolean) statusResponse.get("done");
|
||||
if (Boolean.TRUE.equals(isDone)) {
|
||||
logger.info("VEO: Видео успешно сгенерировано! Парсим ответ...");
|
||||
if (Boolean.TRUE.equals(statusResponse.get("done"))) {
|
||||
logger.info("VEO: Видео готово! Извлекаем байты...");
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> responseData = (Map<String, Object>) statusResponse.get("response");
|
||||
return extractVideoBytes(responseData);
|
||||
@@ -140,7 +172,11 @@ public class GeminiVideoGenerationService {
|
||||
return null;
|
||||
|
||||
} catch (WebClientResponseException e) {
|
||||
logger.error("VEO API Error! HTTP Status: {}. Ответ: {}", e.getStatusCode(), e.getResponseBodyAsString());
|
||||
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);
|
||||
@@ -148,22 +184,6 @@ public class GeminiVideoGenerationService {
|
||||
}
|
||||
}
|
||||
|
||||
private String getAccessTokenFromResources() {
|
||||
try {
|
||||
ClassPathResource resource = new ClassPathResource(CREDENTIALS_FILE_PATH);
|
||||
if (!resource.exists()) return null;
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
GoogleCredentials credentials = GoogleCredentials.fromStream(is)
|
||||
.createScoped("https://www.googleapis.com/auth/cloud-platform");
|
||||
credentials.refreshIfExpired();
|
||||
return credentials.getAccessToken().getTokenValue();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Ошибка чтения ключа: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private byte[] extractVideoBytes(Map<String, Object> responseData) {
|
||||
if (responseData == null) return null;
|
||||
@@ -172,12 +192,17 @@ public class GeminiVideoGenerationService {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
logger.error("VEO: Массив predictions пустой!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("VEO: Ошибка парсинга Base64: {}", e.getMessage());
|
||||
logger.error("VEO: Ошибка при декодировании Base64 видео: {}", e.getMessage(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user