UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

801 lines 33.3 kB
/** * Vertex AI Video Generation Handler * * Standalone module for Veo 3.1 video generation via Vertex AI. * Generates videos from an input image and text prompt. * * Based on Vertex AI Veo 3.1 video generation API * * @module adapters/video/vertexVideoHandler * @see https://cloud.google.com/vertex-ai/generative-ai/docs/video/generate-videos */ import { readFile } from "node:fs/promises"; import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js"; import { TIMEOUTS } from "../../constants/timeouts.js"; import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js"; import { isAbortError, NeuroLinkError, withTimeout, } from "../../utils/errorHandling.js"; import { logger } from "../../utils/logger.js"; // ============================================================================ // VIDEO ERROR CODES (Re-exported for backward compatibility) // ============================================================================ /** * Video error codes - re-exported from constants module for backward compatibility. * @see {@link VIDEO_ERROR_CODES} in constants/videoErrors.ts for definitions */ export { VIDEO_ERROR_CODES }; /** * Video generation error class * Extends NeuroLinkError for consistent error handling across the SDK */ export class VideoError extends NeuroLinkError { constructor(options) { super({ code: options.code, message: options.message, category: options.category ?? ErrorCategory.EXECUTION, severity: options.severity ?? ErrorSeverity.HIGH, retriable: options.retriable ?? false, context: options.context, originalError: options.originalError, }); this.name = "VideoError"; } } // ============================================================================ // CONSTANTS // ============================================================================ /** Timeout for video generation (3 minutes - video gen typically takes 1-2 min) */ const VIDEO_GENERATION_TIMEOUT_MS = 180000; /** Polling interval for checking operation status (5 seconds) */ const POLL_INTERVAL_MS = 5000; /** Full model name for Veo 3.1 (IMPORTANT: not just "veo-3.1") */ const VEO_MODEL = "veo-3.1-generate-001"; /** Full model name for Veo 3.1 Fast (used for transitions) */ const VEO_FAST_MODEL = "veo-3.1-fast-generate-001"; /** Default location for Vertex AI */ const DEFAULT_LOCATION = "us-central1"; // ============================================================================ // CONFIGURATION HELPERS // ============================================================================ /** * Check if Vertex AI is configured for video generation * * @returns True if Google Cloud credentials are available * * @example * ```typescript * if (!isVertexVideoConfigured()) { * console.error("Set GOOGLE_APPLICATION_CREDENTIALS to enable video generation"); * } * ``` */ export function isVertexVideoConfigured() { // Same credential detection as googleVertex.ts hasGoogleCredentials(). // GoogleAuth (used by getAccessToken) also supports ADC from // `gcloud auth application-default login` automatically, so we only // gate on the explicit env vars here — if none are set, we still // allow the call through and let GoogleAuth resolve ADC at runtime. // This avoids duplicating GoogleAuth's discovery logic. return !!(process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_APPLICATION_CREDENTIALS_NEUROLINK || process.env.GOOGLE_SERVICE_ACCOUNT_KEY || (process.env.GOOGLE_AUTH_CLIENT_EMAIL && process.env.GOOGLE_AUTH_PRIVATE_KEY)); } /** * Get Vertex AI project configuration from environment variables or ADC credentials * * @returns Project ID and location for Vertex AI * @throws VideoError if project cannot be determined */ async function getVertexConfig() { // Veo 3.1 is only published in us-central1 (and the synthetic // `global` endpoint). When the operator's environment defaults to // a region where Veo 3.1 isn't available (e.g. us-east5 set for // Gemini chat traffic), the regional endpoint returns a 404 // "Publisher Model … was not found". Allow `GOOGLE_VEO_LOCATION` as // a Veo-specific override and fall through to us-central1 instead // of inheriting the default Vertex region. const location = process.env.GOOGLE_VEO_LOCATION || process.env.GOOGLE_VERTEX_VIDEO_LOCATION || DEFAULT_LOCATION; // Try environment variables first let project = process.env.GOOGLE_VERTEX_PROJECT || process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID || process.env.VERTEX_PROJECT_ID; // Fallback: read from ADC credentials file if (!project && process.env.GOOGLE_APPLICATION_CREDENTIALS) { try { const credData = JSON.parse(await readFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, "utf-8")); project = credData.quota_project_id || credData.project_id; } catch (e) { // Ignore read errors, will throw below if project still not found logger.debug("Failed to read project from credentials file", { error: e instanceof Error ? e.message : String(e), }); } } if (!project) { throw new VideoError({ code: VIDEO_ERROR_CODES.PROVIDER_NOT_CONFIGURED, message: "Google Cloud project not found. Set GOOGLE_VERTEX_PROJECT or GOOGLE_CLOUD_PROJECT environment variable, or ensure ADC credentials contain project_id", category: ErrorCategory.CONFIGURATION, severity: ErrorSeverity.HIGH, retriable: false, context: { missingVar: "GOOGLE_VERTEX_PROJECT", feature: "video-generation", checkedEnvVars: [ "GOOGLE_VERTEX_PROJECT", "GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT_ID", "VERTEX_PROJECT_ID", ], }, }); } return { project, location }; } /** * Get access token for Vertex AI authentication * * Uses google-auth-library (transitive dependency from @google-cloud/vertexai) * to obtain access token from configured credentials. * * @returns Access token string * @throws VideoError if authentication fails */ async function getAccessToken() { try { // google-auth-library is a transitive dependency from @google-cloud/vertexai // Using dynamic import with type assertion for runtime resolution const googleAuthLib = (await import("google-auth-library")); const auth = new googleAuthLib.GoogleAuth({ keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS, scopes: ["https://www.googleapis.com/auth/cloud-platform"], }); const token = await withTimeout(auth.getAccessToken(), TIMEOUTS.PROVIDER.AUTH_MS); if (!token) { throw new VideoError({ code: VIDEO_ERROR_CODES.PROVIDER_NOT_CONFIGURED, message: "Failed to obtain access token from Google Cloud credentials", category: ErrorCategory.CONFIGURATION, severity: ErrorSeverity.HIGH, retriable: false, context: { provider: "vertex", feature: "video-generation" }, }); } return token; } catch (error) { if (error instanceof VideoError) { throw error; } throw new VideoError({ code: VIDEO_ERROR_CODES.PROVIDER_NOT_CONFIGURED, message: `Google Cloud authentication failed: ${error instanceof Error ? error.message : String(error)}`, category: ErrorCategory.CONFIGURATION, severity: ErrorSeverity.HIGH, retriable: false, context: { provider: "vertex", feature: "video-generation" }, originalError: error instanceof Error ? error : undefined, }); } } // ============================================================================ // IMAGE UTILITIES // ============================================================================ /** * Detect MIME type from image buffer magic bytes * * @param image - Image buffer to analyze * @returns MIME type string (defaults to "image/jpeg" if unknown) */ function detectMimeType(image) { // Validate buffer has minimum length for format detection if (image.length < 4) { logger.warn("Image buffer too small for format detection", { size: image.length, }); return "image/jpeg"; } // JPEG: FF D8 FF if (image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff) { return "image/jpeg"; } // PNG: 89 50 4E 47 if (image[0] === 0x89 && image[1] === 0x50 && image[2] === 0x4e && image[3] === 0x47) { return "image/png"; } // WebP: RIFF header (52 49 46 46) + WEBP at offset 8 (57 45 42 50) if (image.length >= 12 && image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 && image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50) { return "image/webp"; } // Default fallback logger.warn("Unknown image format detected, defaulting to image/jpeg", { firstBytes: image.slice(0, 12).toString("hex"), size: image.length, }); return "image/jpeg"; } /** * Calculate video dimensions based on resolution and aspect ratio * * @param resolution - Video resolution ("720p" or "1080p") * @param aspectRatio - Aspect ratio ("16:9" or "9:16") * @returns Width and height dimensions */ function calculateDimensions(resolution, aspectRatio) { if (resolution === "1080p") { return aspectRatio === "9:16" ? { width: 1080, height: 1920 } : { width: 1920, height: 1080 }; } // 720p return aspectRatio === "9:16" ? { width: 720, height: 1280 } : { width: 1280, height: 720 }; } // ============================================================================ // VIDEO GENERATION // ============================================================================ /** * Generate video using Vertex AI Veo 3.1 * * Creates a video from an input image and text prompt using Google's Veo 3.1 model. * The video is generated with optional audio and can be customized for resolution, * duration, and aspect ratio. * * @param image - Input image buffer (JPEG, PNG, or WebP) * @param prompt - Text prompt describing desired video motion/content (max 500 chars) * @param options - Video output options (resolution, length, aspect ratio, audio) * @returns VideoGenerationResult with video buffer and metadata * * @throws {VideoError} When credentials are not configured (PROVIDER_NOT_CONFIGURED) * @throws {VideoError} When API returns an error (GENERATION_FAILED) * @throws {VideoError} When polling times out (POLL_TIMEOUT) * * @example * ```typescript * import { generateVideoWithVertex } from "@juspay/neurolink/adapters/video/vertexVideoHandler"; * import { readFileSync, writeFileSync } from "fs"; * * const image = readFileSync("./input.png"); * const result = await generateVideoWithVertex( * image, * "Smooth cinematic camera movement with dramatic lighting", * { resolution: "720p", length: 6, aspectRatio: "16:9", audio: true } * ); * * writeFileSync("output.mp4", result.data); * ``` */ export async function generateVideoWithVertex(image, prompt, options = {}, region) { // Credential validation is deferred to getAccessToken() which uses // GoogleAuth — it handles env vars, service accounts, AND ADC from // `gcloud auth application-default login` automatically. // Same pattern as googleVertex.ts — no synchronous pre-check needed. const config = await getVertexConfig(); const project = config.project; const location = region || config.location; const startTime = Date.now(); // Set defaults (matching reference implementation) const resolution = options.resolution || "720p"; const durationSeconds = options.length || 6; // 4, 6, or 8 const aspectRatio = options.aspectRatio || "16:9"; const generateAudio = options.audio ?? true; logger.debug("Starting Vertex video generation", { project, location, model: VEO_MODEL, resolution, durationSeconds, aspectRatio, generateAudio, promptLength: prompt.length, imageSize: image.length, }); try { // Encode image to base64 and detect MIME type const imageBase64 = image.toString("base64"); const mimeType = detectMimeType(image); // Get auth token const accessToken = await getAccessToken(); // Construct API request - predictLongRunning endpoint // Global endpoint uses aiplatform.googleapis.com (no region prefix), // same pattern as googleVertex.ts createVertexSettings const apiHost = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`; const endpoint = `https://${apiHost}/v1/projects/${project}/locations/${location}/publishers/google/models/${VEO_MODEL}:predictLongRunning`; // Request body structure (verified working from video.js reference) const requestBody = { instances: [ { prompt: prompt, image: { bytesBase64Encoded: imageBase64, mimeType: mimeType, }, }, ], parameters: { sampleCount: 1, durationSeconds: durationSeconds, aspectRatio: aspectRatio, resolution: resolution, generateAudio: generateAudio, resizeMode: "pad", // "pad" preserves aspect ratio, "crop" fills frame }, }; logger.debug("Sending video generation request", { endpoint }); // Create abort controller for request timeout const controller = new AbortController(); const requestTimeout = setTimeout(() => controller.abort(), 30000); // 30s request timeout // Start long-running operation let response; try { response = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify(requestBody), signal: controller.signal, }); } catch (error) { clearTimeout(requestTimeout); if (isAbortError(error)) { throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: "Video generation request timed out after 30 seconds", category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: true, context: { provider: "vertex", endpoint, timeout: 30000 }, }); } throw error; } clearTimeout(requestTimeout); if (!response.ok) { const errorText = await response.text(); throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: `Vertex API error: ${response.status} - ${errorText}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: response.status >= 500, // 5xx errors are retriable context: { status: response.status, error: errorText, provider: "vertex", endpoint, }, }); } const operation = await response.json(); const operationName = operation.name; if (!operationName) { throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: "Vertex API did not return an operation name", category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { response: operation, provider: "vertex" }, }); } logger.debug("Video generation operation started", { operationName }); // Poll for completion using fetchPredictOperation endpoint const remainingTime = VIDEO_GENERATION_TIMEOUT_MS - (Date.now() - startTime); const videoBuffer = await pollVideoOperation(operationName, accessToken, project, location, Math.max(1000, remainingTime)); const processingTime = Date.now() - startTime; // Calculate dimensions based on resolution and aspect ratio const dimensions = calculateDimensions(resolution, aspectRatio); logger.info("Video generation complete", { processingTime, videoSizeKB: Math.round(videoBuffer.length / 1024), dimensions, }); return { data: videoBuffer, mediaType: "video/mp4", metadata: { duration: durationSeconds, dimensions, model: VEO_MODEL, provider: "vertex", aspectRatio, audioEnabled: generateAudio, processingTime, }, }; } catch (error) { // Re-throw VideoError as-is if (error instanceof VideoError) { throw error; } // Wrap other errors throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: `Video generation failed: ${error instanceof Error ? error.message : String(error)}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: true, context: { provider: "vertex" }, originalError: error instanceof Error ? error : undefined, }); } } // ============================================================================ // POLLING HELPERS // ============================================================================ /** * Vertex AI operation result type for type safety */ /** * Extract video buffer from completed operation result * * @param result - Completed operation result from Vertex AI * @param operationName - Operation name for error context * @returns Video buffer * @throws VideoError if video data is missing or in unexpected format */ function extractVideoFromResult(result, operationName) { // Check for error in completed operation if (result.error) { throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: `Video generation failed: ${result.error.message || JSON.stringify(result.error)}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { operationName, error: result.error, provider: "vertex" }, }); } // Extract video from response - structure is result.response.videos[0] const videoData = result.response?.videos?.[0]; if (!videoData) { throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: "No video data in response from Vertex AI", category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { operationName, response: result.response, provider: "vertex" }, }); } // Video can be returned as base64 or GCS URI if (videoData.gcsUri) { throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: `Video stored at GCS: ${videoData.gcsUri}. GCS download not yet implemented.`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { operationName, gcsUri: videoData.gcsUri, provider: "vertex", suggestion: "Do not set storageUri parameter to receive video as base64 inline", }, }); } if (videoData.bytesBase64Encoded) { return Buffer.from(videoData.bytesBase64Encoded, "base64"); } throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: "No video bytes in response - unexpected response format", category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { operationName, videoData, provider: "vertex" }, }); } /** * Make a poll request to the Vertex AI fetchPredictOperation endpoint * * @param pollEndpoint - Full URL for the poll endpoint * @param operationName - Operation name to poll * @param accessToken - Google Cloud access token * @param timeoutMs - Request timeout in milliseconds (default: 30000) * @returns Response JSON from the poll request * @throws VideoError on request failure */ async function makePollRequest(pollEndpoint, operationName, accessToken, timeoutMs = 30000) { const controller = new AbortController(); const requestTimeout = setTimeout(() => controller.abort(), timeoutMs); let response; try { response = await fetch(pollEndpoint, { method: "POST", // NOTE: POST, not GET! headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify({ operationName }), // Pass operation name in body signal: controller.signal, }); } catch (error) { clearTimeout(requestTimeout); if (isAbortError(error)) { throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: `Poll request timed out after ${timeoutMs}ms`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: true, context: { provider: "vertex", operationName, timeout: timeoutMs }, }); } throw error; } clearTimeout(requestTimeout); if (!response.ok) { const errorText = await response.text(); throw new VideoError({ code: VIDEO_ERROR_CODES.GENERATION_FAILED, message: `Failed to poll video operation: ${response.status} - ${errorText}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: response.status >= 500, context: { operationName, status: response.status, error: errorText, provider: "vertex", }, }); } return response.json(); } // ============================================================================ // POLLING // ============================================================================ /** * Poll Vertex AI operation until complete * * IMPORTANT: Uses fetchPredictOperation endpoint (POST with operationName in body), * NOT the generic operations GET endpoint! * * @param operationName - Full operation name from predictLongRunning response * @param accessToken - Google Cloud access token * @param project - Google Cloud project ID * @param location - Vertex AI location * @param timeoutMs - Maximum time to wait for completion * @returns Video buffer when complete * * @throws {VideoError} On API error, timeout, or missing video data */ async function pollVideoOperation(operationName, accessToken, project, location, timeoutMs) { return pollOperation(VEO_MODEL, operationName, accessToken, project, location, timeoutMs); } // ============================================================================ // TRANSITION GENERATION (Director Mode) // ============================================================================ /** * Generate a transition clip using Veo 3.1 Fast's first-and-last-frame interpolation. * * This calls the Veo API with both `image` (first frame) and `lastFrame` (last frame), * producing a video that smoothly interpolates between the two frames. * * @param firstFrame - JPEG buffer of the first frame (last frame of clip N) * @param lastFrame - JPEG buffer of the last frame (first frame of clip N+1) * @param prompt - Transition prompt describing desired visual flow * @param options - Video output options (resolution, aspect ratio, audio) * @param durationSeconds - Duration of the transition clip (4, 6, or 8) * @param region - Vertex AI region override * @returns Video buffer of the transition clip * * @throws {VideoError} When API returns an error or polling times out */ export async function generateTransitionWithVertex(firstFrame, lastFrame, prompt, options = {}, durationSeconds = 4, region) { const config = await getVertexConfig(); const project = config.project; const location = region || config.location; const startTime = Date.now(); const aspectRatio = options.aspectRatio || "16:9"; const resolution = options.resolution || "720p"; const generateAudio = options.audio ?? true; logger.debug("Starting transition clip generation", { model: VEO_FAST_MODEL, durationSeconds, firstFrameSize: firstFrame.length, lastFrameSize: lastFrame.length, promptLength: prompt.length, }); try { const firstFrameBase64 = firstFrame.toString("base64"); const lastFrameBase64 = lastFrame.toString("base64"); const firstMime = detectMimeType(firstFrame); const lastMime = detectMimeType(lastFrame); const accessToken = await getAccessToken(); // Use Veo 3.1 Fast for transitions (faster with minimal quality difference) const endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${VEO_FAST_MODEL}:predictLongRunning`; const requestBody = { instances: [ { prompt: prompt, image: { bytesBase64Encoded: firstFrameBase64, mimeType: firstMime, }, lastFrame: { bytesBase64Encoded: lastFrameBase64, mimeType: lastMime, }, }, ], parameters: { sampleCount: 1, durationSeconds: durationSeconds, aspectRatio: aspectRatio, resolution: resolution, generateAudio: generateAudio, }, }; const controller = new AbortController(); const requestTimeout = setTimeout(() => controller.abort(), 30000); let response; try { response = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json; charset=utf-8", }, body: JSON.stringify(requestBody), signal: controller.signal, }); } catch (error) { clearTimeout(requestTimeout); if (isAbortError(error)) { throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED, message: "Transition generation request timed out after 30 seconds", category: ErrorCategory.EXECUTION, severity: ErrorSeverity.MEDIUM, retriable: true, }); } throw error; } clearTimeout(requestTimeout); if (!response.ok) { const errorText = await response.text(); throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED, message: `Transition API error: ${response.status} - ${errorText}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.MEDIUM, retriable: response.status >= 500, context: { status: response.status, error: errorText }, }); } const operation = await response.json(); const operationName = operation.name; if (!operationName) { throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED, message: "Transition API did not return an operation name", category: ErrorCategory.EXECUTION, severity: ErrorSeverity.MEDIUM, retriable: false, }); } // Poll with Veo Fast model endpoint const remainingTime = VIDEO_GENERATION_TIMEOUT_MS - (Date.now() - startTime); const videoBuffer = await pollTransitionOperation(operationName, accessToken, project, location, Math.max(1000, remainingTime)); logger.debug("Transition clip generated", { processingTime: Date.now() - startTime, videoSize: videoBuffer.length, }); return videoBuffer; } catch (error) { if (error instanceof VideoError) { throw error; } throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED, message: `Transition generation failed: ${error instanceof Error ? error.message : String(error)}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.MEDIUM, retriable: true, originalError: error instanceof Error ? error : undefined, }); } } /** * Common polling helper that handles both video and transition operations. * Accepts a model name to construct the appropriate endpoint. */ async function pollOperation(modelOrEndpoint, operationName, accessToken, project, location, timeoutMs) { const startTime = Date.now(); // Global endpoint uses aiplatform.googleapis.com (no region prefix), // same pattern as the predictLongRunning endpoint at line 374 const pollHost = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`; const pollEndpoint = `https://${pollHost}/v1/projects/${project}/locations/${location}/publishers/google/models/${modelOrEndpoint}:fetchPredictOperation`; while (Date.now() - startTime < timeoutMs) { const result = await makePollRequest(pollEndpoint, operationName, accessToken); if (result.done) { return extractVideoFromResult(result, operationName); } logger.debug("Polling operation...", { operationName, elapsed: Date.now() - startTime, }); await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); } throw new VideoError({ code: VIDEO_ERROR_CODES.POLL_TIMEOUT, message: `Operation timed out after ${Math.round(timeoutMs / 1000)}s`, category: ErrorCategory.TIMEOUT, severity: ErrorSeverity.MEDIUM, retriable: true, context: { operationName, timeoutMs }, }); } /** * Poll Vertex AI operation for transition clip completion. * Uses the Veo Fast model fetchPredictOperation endpoint. */ async function pollTransitionOperation(operationName, accessToken, project, location, timeoutMs) { return pollOperation(VEO_FAST_MODEL, operationName, accessToken, project, location, timeoutMs); } /** * Class wrapper around the standalone Vertex Veo functions, conforming to * the `VideoHandler` contract so it can register with `VideoProcessor`. * * The free functions (`generateVideoWithVertex`, `generateTransitionWithVertex`, * `isVertexVideoConfigured`) are kept exported for backward compatibility — * external callers (Director's `directorPipeline.ts`, test scripts) reference * them directly. */ export class VertexVideoHandler { maxDurationSeconds = 8; supportedAspectRatios = [ "9:16", "16:9", ]; supportedResolutions = [ "720p", "1080p", ]; isConfigured() { return isVertexVideoConfigured(); } generate(image, prompt, options, region) { return generateVideoWithVertex(image, prompt, options, region); } generateTransition(firstFrame, lastFrame, prompt, options, region) { return generateTransitionWithVertex(firstFrame, lastFrame, prompt, { aspectRatio: options?.aspectRatio, resolution: options?.resolution, audio: options?.audio, }, options?.durationSeconds ?? 4, region); } } //# sourceMappingURL=vertexVideoHandler.js.map