Google: Add Veo models (#4742) * Google: Add Veo models * Combine retry into a singe loop * Improve error logging and reporting * Add client abort handling for video generation endpoint
Signed| @@ -337,6 +337,7 @@ const defaultSettings = { | |||
| 337 | // Google settings | 337 | // Google settings |
| 338 | google_api: 'makersuite', | 338 | google_api: 'makersuite', |
| 339 | google_enhance: true, | 339 | google_enhance: true, |
| 340 | google_duration: 6, | ||
| 340 | }; | 341 | }; |
| 341 | 342 | ||
| 342 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); | 343 | const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed); |
| @@ -526,6 +527,7 @@ async function loadSettings() { | |||
| 526 | $('#sd_bfl_upsampling').prop('checked', extension_settings.sd.bfl_upsampling); | 527 | $('#sd_bfl_upsampling').prop('checked', extension_settings.sd.bfl_upsampling); |
| 527 | $('#sd_google_api').val(extension_settings.sd.google_api); | 528 | $('#sd_google_api').val(extension_settings.sd.google_api); |
| 528 | $('#sd_google_enhance').prop('checked', extension_settings.sd.google_enhance); | 529 | $('#sd_google_enhance').prop('checked', extension_settings.sd.google_enhance); |
| 530 | $('#sd_google_duration').val(extension_settings.sd.google_duration); | ||
| 529 | 531 | ||
| 530 | for (const style of extension_settings.sd.styles) { | 532 | for (const style of extension_settings.sd.styles) { |
| 531 | const option = document.createElement('option'); | 533 | const option = document.createElement('option'); |
| @@ -2149,6 +2151,9 @@ async function loadNovelModels() { | |||
| 2149 | 2151 | ||
| 2150 | async function loadGoogleModels() { | 2152 | async function loadGoogleModels() { |
| 2151 | return [ | 2153 | return [ |
| 2154 | 'imagen-4.0-generate-001', | ||
| 2155 | 'imagen-4.0-ultra-generate-001', | ||
| 2156 | 'imagen-4.0-fast-generate-001', | ||
| 2152 | 'imagen-4.0-generate-preview-06-06', | 2157 | 'imagen-4.0-generate-preview-06-06', |
| 2153 | 'imagen-4.0-fast-generate-preview-06-06', | 2158 | 'imagen-4.0-fast-generate-preview-06-06', |
| 2154 | 'imagen-4.0-ultra-generate-preview-06-06', | 2159 | 'imagen-4.0-ultra-generate-preview-06-06', |
| @@ -2159,6 +2164,13 @@ async function loadGoogleModels() { | |||
| 2159 | 'imagegeneration@006', | 2164 | 'imagegeneration@006', |
| 2160 | 'imagegeneration@005', | 2165 | 'imagegeneration@005', |
| 2161 | 'imagegeneration@002', | 2166 | 'imagegeneration@002', |
| 2167 | 'veo-3.1-generate-preview', | ||
| 2168 | 'veo-3.1-fast-generate-preview', | ||
| 2169 | 'veo-3.0-generate-001', | ||
| 2170 | 'veo-3.0-fast-generate-001', | ||
| 2171 | 'veo-2.0-generate-001', | ||
| 2172 | 'veo-2.0-generate-exp', | ||
| 2173 | 'veo-2.0-generate-preview', | ||
| 2162 | ].map(name => ({ value: name, text: name })); | 2174 | ].map(name => ({ value: name, text: name })); |
| 2163 | } | 2175 | } |
| 2164 | 2176 | ||
| @@ -3889,6 +3901,38 @@ async function generateFalaiImage(prompt, negativePrompt, signal) { | |||
| 3889 | * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete. | 3901 | * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete. |
| 3890 | */ | 3902 | */ |
| 3891 | async function generateGoogleImage(prompt, negativePrompt, signal) { | 3903 | async function generateGoogleImage(prompt, negativePrompt, signal) { |
| 3904 | const isVeo = /^veo-/.test(extension_settings.sd.model); | ||
| 3905 | |||
| 3906 | if (isVeo) { | ||
| 3907 | const aspectRatio = extension_settings.sd.width / extension_settings.sd.height; | ||
| 3908 | const maxPromptLength = 3000; // 1024 tokens approx. | ||
| 3909 | const videoResult = await fetch('/api/google/generate-video', { | ||
| 3910 | method: 'POST', | ||
| 3911 | headers: getRequestHeaders(), | ||
| 3912 | signal: signal, | ||
| 3913 | body: JSON.stringify({ | ||
| 3914 | prompt: prompt.slice(0, maxPromptLength), | ||
| 3915 | aspect_ratio: aspectRatio >= 1 ? '16:9' : '9:16', | ||
| 3916 | seconds: extension_settings.sd.google_duration, | ||
| 3917 | negative_prompt: negativePrompt, | ||
| 3918 | model: extension_settings.sd.model, | ||
| 3919 | api: extension_settings.sd.google_api || 'makersuite', | ||
| 3920 | seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | ||
| 3921 | vertexai_auth_mode: oai_settings.vertexai_auth_mode, | ||
| 3922 | vertexai_region: oai_settings.vertexai_region, | ||
| 3923 | vertexai_express_project_id: oai_settings.vertexai_express_project_id, | ||
| 3924 | }), | ||
| 3925 | }); | ||
| 3926 | |||
| 3927 | if (!videoResult.ok) { | ||
| 3928 | const text = await videoResult.text(); | ||
| 3929 | throw new Error(text); | ||
| 3930 | } | ||
| 3931 | |||
| 3932 | const data = await videoResult.json(); | ||
| 3933 | return { format: 'mp4', data: data.video }; | ||
| 3934 | } | ||
| 3935 | |||
| 3892 | const result = await fetch('/api/google/generate-image', { | 3936 | const result = await fetch('/api/google/generate-image', { |
| 3893 | method: 'POST', | 3937 | method: 'POST', |
| 3894 | headers: getRequestHeaders(), | 3938 | headers: getRequestHeaders(), |
| @@ -4837,6 +4881,10 @@ jQuery(async () => { | |||
| 4837 | extension_settings.sd.google_enhance = $(this).prop('checked'); | 4881 | extension_settings.sd.google_enhance = $(this).prop('checked'); |
| 4838 | saveSettingsDebounced(); | 4882 | saveSettingsDebounced(); |
| 4839 | }); | 4883 | }); |
| 4884 | $('#sd_google_duration').on('input', function () { | ||
| 4885 | extension_settings.sd.google_duration = Number($(this).val()); | ||
| 4886 | saveSettingsDebounced(); | ||
| 4887 | }); | ||
| 4840 | $('#sd_electronhub_quality').on('change', function () { | 4888 | $('#sd_electronhub_quality').on('change', function () { |
| 4841 | extension_settings.sd.electronhub_quality = String($(this).val()); | 4889 | extension_settings.sd.electronhub_quality = String($(this).val()); |
| 4842 | saveSettingsDebounced(); | 4890 | saveSettingsDebounced(); |
| @@ -302,13 +302,21 @@ | |||
| 302 | </select> | 302 | </select> |
| 303 | </div> | 303 | </div> |
| 304 | </div> | 304 | </div> |
| 305 | <div class="flex-container"> | 305 | <div class="flex-container alignItemsCenter"> |
| 306 | <label class="flex1 checkbox_label" for="sd_google_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail)."> | 306 | <label class="flex1 checkbox_label" for="sd_google_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail)."> |
| 307 | <input id="sd_google_enhance" type="checkbox" /> | 307 | <input id="sd_google_enhance" type="checkbox" /> |
| 308 | <span data-i18n="Enhance"> | 308 | <span data-i18n="Enhance"> |
| 309 | Enhance | 309 | Enhance |
| 310 | </span> | 310 | </span> |
| 311 | </label> | 311 | </label> |
| 312 | <div class="flex1"> | ||
| 313 | <label for="sd_google_duration" data-i18n="Duration (Veo)">Duration (Veo)</label> | ||
| 314 | <select id="sd_google_duration"> | ||
| 315 | <option value="4">Short (4 seconds)</option> | ||
| 316 | <option value="6">Medium (6 seconds)</option> | ||
| 317 | <option value="8">Long (8 seconds)</option> | ||
| 318 | </select> | ||
| 319 | </div> | ||
| 312 | </div> | 320 | </div> |
| 313 | </div> | 321 | </div> |
| 314 | 322 | ||
| @@ -3,10 +3,13 @@ import fetch from 'node-fetch'; | |||
| 3 | import express from 'express'; | 3 | import express from 'express'; |
| 4 | import { speak, languages } from 'google-translate-api-x'; | 4 | import { speak, languages } from 'google-translate-api-x'; |
| 5 | import crypto from 'node:crypto'; | 5 | import crypto from 'node:crypto'; |
| 6 | import util from 'node:util'; | ||
| 7 | import urlJoin from 'url-join'; | ||
| 8 | import lodash from 'lodash'; | ||
| 6 | 9 | ||
| 7 | import { readSecret, SECRET_KEYS } from './secrets.js'; | 10 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 8 | import { GEMINI_SAFETY } from '../constants.js'; | 11 | import { GEMINI_SAFETY } from '../constants.js'; |
| 9 | import { getConfigValue, trimTrailingSlash } from '../util.js'; | 12 | import { delay, getConfigValue, trimTrailingSlash } from '../util.js'; |
| 10 | 13 | ||
| 11 | const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; | 14 | const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; |
| 12 | const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com'; | 15 | const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com'; |
| @@ -156,7 +159,7 @@ export function getProjectIdFromServiceAccount(serviceAccount) { | |||
| 156 | * @param {express.Request} request Express request object | 159 | * @param {express.Request} request Express request object |
| 157 | * @param {string} model Model name to use | 160 | * @param {string} model Model name to use |
| 158 | * @param {string} endpoint API endpoint (default: 'generateContent') | 161 | * @param {string} endpoint API endpoint (default: 'generateContent') |
| 159 | * @returns {Promise<{url: string, headers: object, apiName: string}>} URL, headers, and API name | 162 | * @returns {Promise<{url: string, headers: object, apiName: string, baseUrl: string}>} URL, headers, and API name |
| 160 | */ | 163 | */ |
| 161 | export async function getGoogleApiConfig(request, model, endpoint = 'generateContent') { | 164 | export async function getGoogleApiConfig(request, model, endpoint = 'generateContent') { |
| 162 | const useVertexAi = request.body.api === 'vertexai'; | 165 | const useVertexAi = request.body.api === 'vertexai'; |
| @@ -164,6 +167,7 @@ export async function getGoogleApiConfig(request, model, endpoint = 'generateCon | |||
| 164 | const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio'; | 167 | const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio'; |
| 165 | 168 | ||
| 166 | let url; | 169 | let url; |
| 170 | let baseUrl; | ||
| 167 | let headers = { | 171 | let headers = { |
| 168 | 'Content-Type': 'application/json', | 172 | 'Content-Type': 'application/json', |
| 169 | }; | 173 | }; |
| @@ -176,12 +180,13 @@ export async function getGoogleApiConfig(request, model, endpoint = 'generateCon | |||
| 176 | // Express mode: use API key parameter | 180 | // Express mode: use API key parameter |
| 177 | const keyParam = authHeader.replace('Bearer ', ''); | 181 | const keyParam = authHeader.replace('Bearer ', ''); |
| 178 | const projectId = request.body.vertexai_express_project_id; | 182 | const projectId = request.body.vertexai_express_project_id; |
| 179 | const baseUrl = region === 'global' | 183 | baseUrl = region === 'global' |
| 180 | ? 'https://aiplatform.googleapis.com' | 184 | ? 'https://aiplatform.googleapis.com/v1' |
| 181 | : `https://${region}-aiplatform.googleapis.com`; | 185 | : `https://${region}-aiplatform.googleapis.com/v1`; |
| 182 | url = projectId | 186 | url = projectId |
| 183 | ? `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}?key=${keyParam}` | 187 | ? `${baseUrl}/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}` |
| 184 | : `${baseUrl}/v1/publishers/google/models/${model}:${endpoint}?key=${keyParam}`; | 188 | : `${baseUrl}/publishers/google/models/${model}:${endpoint}`; |
| 189 | headers['x-goog-api-key'] = keyParam; | ||
| 185 | } else if (authType === 'full') { | 190 | } else if (authType === 'full') { |
| 186 | // Full mode: use project-specific URL with Authorization header | 191 | // Full mode: use project-specific URL with Authorization header |
| 187 | // Get project ID from Service Account JSON | 192 | // Get project ID from Service Account JSON |
| @@ -198,14 +203,16 @@ export async function getGoogleApiConfig(request, model, endpoint = 'generateCon | |||
| 198 | throw new Error('Failed to extract project ID from Service Account JSON.'); | 203 | throw new Error('Failed to extract project ID from Service Account JSON.'); |
| 199 | } | 204 | } |
| 200 | // Handle global region differently - no region prefix in hostname | 205 | // Handle global region differently - no region prefix in hostname |
| 201 | url = region === 'global' | 206 | baseUrl = region === 'global' |
| 202 | ? `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}` | 207 | ? 'https://aiplatform.googleapis.com/v1' |
| 203 | : `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}`; | 208 | : `https://${region}-aiplatform.googleapis.com/v1`; |
| 209 | url = `${baseUrl}/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${endpoint}`; | ||
| 204 | headers['Authorization'] = authHeader; | 210 | headers['Authorization'] = authHeader; |
| 205 | } else { | 211 | } else { |
| 206 | // Proxy mode: use Authorization header | 212 | // Proxy mode: use Authorization header |
| 207 | const apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_VERTEX_AI); | 213 | const apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_VERTEX_AI); |
| 208 | url = `${apiUrl}/v1/publishers/google/models/${model}:${endpoint}`; | 214 | baseUrl = `${apiUrl}/v1`; |
| 215 | url = `${baseUrl}/publishers/google/models/${model}:${endpoint}`; | ||
| 209 | headers['Authorization'] = authHeader; | 216 | headers['Authorization'] = authHeader; |
| 210 | } | 217 | } |
| 211 | } else { | 218 | } else { |
| @@ -213,10 +220,12 @@ export async function getGoogleApiConfig(request, model, endpoint = 'generateCon | |||
| 213 | const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); | 220 | const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); |
| 214 | const apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_MAKERSUITE); | 221 | const apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_MAKERSUITE); |
| 215 | const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta'); | 222 | const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta'); |
| 216 | url = `${apiUrl}/${apiVersion}/models/${model}:${endpoint}?key=${apiKey}`; | 223 | baseUrl = `${apiUrl}/${apiVersion}`; |
| 224 | url = `${baseUrl}/models/${model}:${endpoint}`; | ||
| 225 | headers['x-goog-api-key'] = apiKey; | ||
| 217 | } | 226 | } |
| 218 | 227 | ||
| 219 | return { url, headers, apiName }; | 228 | return { url, headers, apiName, baseUrl }; |
| 220 | } | 229 | } |
| 221 | 230 | ||
| 222 | export const router = express.Router(); | 231 | export const router = express.Router(); |
| @@ -461,7 +470,7 @@ router.post('/generate-image', async (request, response) => { | |||
| 461 | if (!result.ok) { | 470 | if (!result.ok) { |
| 462 | const errorText = await result.text(); | 471 | const errorText = await result.text(); |
| 463 | console.warn(`${apiName} image generation error: ${result.status} ${result.statusText}`, errorText); | 472 | console.warn(`${apiName} image generation error: ${result.status} ${result.statusText}`, errorText); |
| 464 | return response.sendStatus(500); | 473 | return response.status(500).send('Image generation request failed'); |
| 465 | } | 474 | } |
| 466 | 475 | ||
| 467 | /** @type {any} */ | 476 | /** @type {any} */ |
| @@ -470,7 +479,7 @@ router.post('/generate-image', async (request, response) => { | |||
| 470 | 479 | ||
| 471 | if (!imagePart) { | 480 | if (!imagePart) { |
| 472 | console.warn(`${apiName} image generation error: No image data found in response`); | 481 | console.warn(`${apiName} image generation error: No image data found in response`); |
| 473 | return response.sendStatus(500); | 482 | return response.status(500).send('No image data found in response'); |
| 474 | } | 483 | } |
| 475 | 484 | ||
| 476 | return response.send({ image: imagePart }); | 485 | return response.send({ image: imagePart }); |
| @@ -482,3 +491,148 @@ router.post('/generate-image', async (request, response) => { | |||
| 482 | return response.end(); | 491 | return response.end(); |
| 483 | } | 492 | } |
| 484 | }); | 493 | }); |
| 494 | |||
| 495 | router.post('/generate-video', async (request, response) => { | ||
| 496 | try { | ||
| 497 | const controller = new AbortController(); | ||
| 498 | request.socket.removeAllListeners('close'); | ||
| 499 | request.socket.on('close', function () { | ||
| 500 | controller.abort(); | ||
| 501 | }); | ||
| 502 | |||
| 503 | const model = request.body.model || 'veo-3.1-generate-preview'; | ||
| 504 | const { url, headers, apiName, baseUrl } = await getGoogleApiConfig(request, model, 'predictLongRunning'); | ||
| 505 | const useVertexAi = request.body.api === 'vertexai'; | ||
| 506 | |||
| 507 | const isVeo3 = /veo-3/.test(model); | ||
| 508 | const lowerBound = isVeo3 ? 4 : 5; | ||
| 509 | const upperBound = isVeo3 ? 8 : 8; | ||
| 510 | |||
| 511 | const requestBody = { | ||
| 512 | instances: [{ | ||
| 513 | prompt: String(request.body.prompt || ''), | ||
| 514 | }], | ||
| 515 | parameters: { | ||
| 516 | negativePrompt: String(request.body.negative_prompt || ''), | ||
| 517 | durationSeconds: lodash.clamp(Number(request.body.seconds || 6), lowerBound, upperBound), | ||
| 518 | aspectRatio: String(request.body.aspect_ratio || '16:9'), | ||
| 519 | personGeneration: 'allow_all', | ||
| 520 | seed: isVeo3 ? Number(request.body.seed ?? Math.floor(Math.random() * 1000000)) : undefined, | ||
| 521 | }, | ||
| 522 | }; | ||
| 523 | |||
| 524 | console.debug(`${apiName} video generation request:`, model, requestBody); | ||
| 525 | const videoJobResponse = await fetch(url, { | ||
| 526 | method: 'POST', | ||
| 527 | headers: headers, | ||
| 528 | body: JSON.stringify(requestBody), | ||
| 529 | }); | ||
| 530 | |||
| 531 | if (!videoJobResponse.ok) { | ||
| 532 | const errorText = await videoJobResponse.text(); | ||
| 533 | console.warn(`${apiName} video generation error: ${videoJobResponse.status} ${videoJobResponse.statusText}`, errorText); | ||
| 534 | return response.status(500).send('Video generation request failed'); | ||
| 535 | } | ||
| 536 | |||
| 537 | /** @type {any} */ | ||
| 538 | const videoJobData = await videoJobResponse.json(); | ||
| 539 | const videoJobName = videoJobData?.name; | ||
| 540 | |||
| 541 | if (!videoJobName) { | ||
| 542 | console.warn(`${apiName} video generation error: No job name found in response`); | ||
| 543 | return response.status(500).send('No video job name found in response'); | ||
| 544 | } | ||
| 545 | |||
| 546 | console.debug(`${apiName} video job name:`, videoJobName); | ||
| 547 | |||
| 548 | for (let attempt = 0; attempt < 30; attempt++) { | ||
| 549 | if (controller.signal.aborted) { | ||
| 550 | console.info(`${apiName} video generation aborted by client`); | ||
| 551 | return response.status(500).send('Video generation aborted by client'); | ||
| 552 | } | ||
| 553 | |||
| 554 | await delay(5000 + attempt * 1000); | ||
| 555 | |||
| 556 | if (useVertexAi) { | ||
| 557 | const { url: pollUrl, headers: pollHeaders } = await getGoogleApiConfig(request, model, 'fetchPredictOperation'); | ||
| 558 | |||
| 559 | const pollResponse = await fetch(pollUrl, { | ||
| 560 | method: 'POST', | ||
| 561 | headers: pollHeaders, | ||
| 562 | body: JSON.stringify({ operationName: videoJobName }), | ||
| 563 | }); | ||
| 564 | |||
| 565 | if (!pollResponse.ok) { | ||
| 566 | const errorText = await pollResponse.text(); | ||
| 567 | console.warn(`${apiName} video job status error: ${pollResponse.status} ${pollResponse.statusText}`, errorText); | ||
| 568 | return response.status(500).send('Video job status request failed'); | ||
| 569 | } | ||
| 570 | |||
| 571 | /** @type {any} */ | ||
| 572 | const pollData = await pollResponse.json(); | ||
| 573 | const jobDone = pollData?.done; | ||
| 574 | console.debug(`${apiName} video job status attempt ${attempt + 1}: ${jobDone ? 'done' : 'running'}`); | ||
| 575 | |||
| 576 | if (jobDone) { | ||
| 577 | const videoData = pollData?.response?.videos?.[0]?.bytesBase64Encoded; | ||
| 578 | if (!videoData) { | ||
| 579 | const pollDataLog = util.inspect(pollData, { depth: 5, colors: true, maxStringLength: 500 }); | ||
| 580 | console.warn(`${apiName} video generation error: No video data found in response`, pollDataLog); | ||
| 581 | return response.status(500).send('No video data found in response'); | ||
| 582 | } | ||
| 583 | |||
| 584 | return response.send({ video: videoData }); | ||
| 585 | } | ||
| 586 | } else { | ||
| 587 | const pollUrl = urlJoin(baseUrl, videoJobName); | ||
| 588 | const pollResponse = await fetch(pollUrl, { | ||
| 589 | method: 'GET', | ||
| 590 | headers: headers, | ||
| 591 | }); | ||
| 592 | |||
| 593 | if (!pollResponse.ok) { | ||
| 594 | const errorText = await pollResponse.text(); | ||
| 595 | console.warn(`${apiName} video job status error: ${pollResponse.status} ${pollResponse.statusText}`, errorText); | ||
| 596 | return response.status(500).send('Video job status request failed'); | ||
| 597 | } | ||
| 598 | |||
| 599 | /** @type {any} */ | ||
| 600 | const pollData = await pollResponse.json(); | ||
| 601 | const jobDone = pollData?.done; | ||
| 602 | console.debug(`${apiName} video job status attempt ${attempt + 1}: ${jobDone ? 'done' : 'running'}`); | ||
| 603 | |||
| 604 | if (jobDone) { | ||
| 605 | const videoUri = pollData?.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri; | ||
| 606 | console.debug(`${apiName} video URI:`, videoUri); | ||
| 607 | |||
| 608 | if (!videoUri) { | ||
| 609 | const pollDataLog = util.inspect(pollData, { depth: 5, colors: true, maxStringLength: 500 }); | ||
| 610 | console.warn(`${apiName} video generation error: No video URI found in response`, pollDataLog); | ||
| 611 | return response.status(500).send('No video URI found in response'); | ||
| 612 | } | ||
| 613 | |||
| 614 | const videoResponse = await fetch(videoUri, { | ||
| 615 | method: 'GET', | ||
| 616 | headers: headers, | ||
| 617 | }); | ||
| 618 | |||
| 619 | if (!videoResponse.ok) { | ||
| 620 | console.warn(`${apiName} video fetch error: ${videoResponse.status} ${videoResponse.statusText}`); | ||
| 621 | return response.status(500).send('Video fetch request failed'); | ||
| 622 | } | ||
| 623 | |||
| 624 | const videoData = await videoResponse.arrayBuffer(); | ||
| 625 | const videoBase64 = Buffer.from(videoData).toString('base64'); | ||
| 626 | |||
| 627 | return response.send({ video: videoBase64 }); | ||
| 628 | } | ||
| 629 | } | ||
| 630 | } | ||
| 631 | |||
| 632 | console.warn(`${apiName} video generation error: Job timed out after multiple attempts`); | ||
| 633 | return response.status(500).send('Video generation timed out'); | ||
| 634 | } catch (error) { | ||
| 635 | console.error('Google Video generation failed:', error); | ||
| 636 | return response.sendStatus(500); | ||
| 637 | } | ||
| 638 | }); | ||