| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import type { |
| VideoGenerationConfig, |
| VideoGenerationOptions, |
| VideoGenerationResult, |
| } from '../types'; |
|
|
| const DEFAULT_MODEL = 'veo-3.0-generate-001'; |
| const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com'; |
| const POLL_INTERVAL_MS = 10_000; |
| const MAX_POLL_ATTEMPTS = 60; |
|
|
| function delay(ms: number): Promise<void> { |
| return new Promise((resolve) => setTimeout(resolve, ms)); |
| } |
|
|
| |
| function getDimensions(aspectRatio?: string): { |
| width: number; |
| height: number; |
| } { |
| switch (aspectRatio) { |
| case '9:16': |
| return { width: 720, height: 1280 }; |
| case '1:1': |
| return { width: 1080, height: 1080 }; |
| case '4:3': |
| return { width: 1024, height: 768 }; |
| default: |
| return { width: 1280, height: 720 }; |
| } |
| } |
|
|
| |
| function apiHeaders(apiKey: string): Record<string, string> { |
| return { |
| 'Content-Type': 'application/json', |
| 'x-goog-api-key': apiKey, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| interface VeoOperation { |
| name: string; |
| done?: boolean; |
| response?: { |
| |
| videos?: Array<{ |
| bytesBase64Encoded?: string; |
| mimeType?: string; |
| }>; |
| }; |
| error?: { code: number; message: string; status: string }; |
| } |
|
|
| |
| |
| |
|
|
| async function submitVideoGeneration( |
| baseUrl: string, |
| apiKey: string, |
| model: string, |
| options: VideoGenerationOptions, |
| ): Promise<VeoOperation> { |
| const url = `${baseUrl}/v1beta/models/${model}:predictLongRunning`; |
|
|
| const body: Record<string, unknown> = { |
| instances: [{ prompt: options.prompt }], |
| }; |
|
|
| |
| const parameters: Record<string, unknown> = {}; |
| if (options.aspectRatio) parameters.aspectRatio = options.aspectRatio; |
| if (options.duration) parameters.durationSeconds = options.duration; |
| if (Object.keys(parameters).length > 0) { |
| body.parameters = parameters; |
| } |
|
|
| const response = await fetch(url, { |
| method: 'POST', |
| headers: apiHeaders(apiKey), |
| body: JSON.stringify(body), |
| }); |
|
|
| if (!response.ok) { |
| const text = await response.text(); |
| throw new Error(`Veo submit failed (${response.status}): ${text}`); |
| } |
|
|
| return response.json() as Promise<VeoOperation>; |
| } |
|
|
| |
| |
| |
|
|
| async function pollOperation( |
| baseUrl: string, |
| apiKey: string, |
| model: string, |
| operationName: string, |
| ): Promise<VeoOperation> { |
| const url = `${baseUrl}/v1beta/models/${model}:fetchPredictOperation`; |
|
|
| const response = await fetch(url, { |
| method: 'POST', |
| headers: apiHeaders(apiKey), |
| body: JSON.stringify({ operationName }), |
| }); |
|
|
| if (!response.ok) { |
| const text = await response.text(); |
| throw new Error(`Veo poll failed (${response.status}): ${text}`); |
| } |
|
|
| return response.json() as Promise<VeoOperation>; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| export async function testVeoConnectivity( |
| config: VideoGenerationConfig, |
| ): Promise<{ success: boolean; message: string }> { |
| const model = config.model || DEFAULT_MODEL; |
| const baseUrl = config.baseUrl || DEFAULT_BASE_URL; |
| const url = `${baseUrl}/v1beta/models`; |
|
|
| |
| let response: Response | null = null; |
| try { |
| response = await fetch(`${url}?key=${config.apiKey}`, { method: 'GET' }); |
| } catch { |
| |
| } |
| if (!response || !response.ok) { |
| try { |
| response = await fetch(url, { |
| method: 'GET', |
| headers: { 'x-goog-api-key': config.apiKey }, |
| }); |
| } catch (_err) { |
| return { |
| success: false, |
| message: `Network error: unable to reach ${baseUrl}. Check your Base URL and network connection.`, |
| }; |
| } |
| } |
|
|
| if (response.ok) { |
| return { success: true, message: `Connected to Veo (${model})` }; |
| } |
|
|
| |
| const text = await response.text().catch(() => ''); |
| if (response.status === 400 || response.status === 401 || response.status === 403) { |
| return { |
| success: false, |
| message: `Invalid API key or unauthorized (${response.status}). Check your API Key and Base URL match the same provider.`, |
| }; |
| } |
| return { |
| success: false, |
| message: `Veo connectivity failed (${response.status}): ${text}`, |
| }; |
| } |
|
|
| export async function generateWithVeo( |
| config: VideoGenerationConfig, |
| options: VideoGenerationOptions, |
| ): Promise<VideoGenerationResult> { |
| const model = config.model || DEFAULT_MODEL; |
| const baseUrl = config.baseUrl || DEFAULT_BASE_URL; |
|
|
| |
| const operation = await submitVideoGeneration(baseUrl, config.apiKey, model, options); |
|
|
| if (!operation.name) { |
| throw new Error('Veo returned operation without name'); |
| } |
|
|
| |
| let current = operation; |
| let pollCount = 0; |
| while (!current.done) { |
| if (pollCount >= MAX_POLL_ATTEMPTS) { |
| throw new Error('Veo video generation timed out after 10 minutes'); |
| } |
| await delay(POLL_INTERVAL_MS); |
| current = await pollOperation(baseUrl, config.apiKey, model, current.name); |
| pollCount++; |
| } |
|
|
| |
| if (current.error) { |
| throw new Error(`Veo generation failed: ${current.error.code} - ${current.error.message}`); |
| } |
|
|
| |
| const videos = current.response?.videos; |
| if (!videos || videos.length === 0) { |
| throw new Error('Veo returned no generated videos'); |
| } |
|
|
| const first = videos[0]; |
| if (!first.bytesBase64Encoded) { |
| throw new Error('Veo returned video entry without data'); |
| } |
|
|
| const base64 = first.bytesBase64Encoded; |
| const mimeType = first.mimeType || 'video/mp4'; |
|
|
| const { width, height } = getDimensions(options.aspectRatio); |
|
|
| return { |
| url: `data:${mimeType};base64,${base64}`, |
| duration: options.duration || 8, |
| width, |
| height, |
| }; |
| } |
|
|