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

144 lines 5.48 kB
/** * Frame Extractor for Director Mode * * Extracts first and last frames from MP4 video buffers using FFmpeg. * Used by Director Mode to obtain boundary frames for Veo 3.1 * first-and-last-frame interpolation transitions. * * Uses the shared FFmpeg adapter for binary resolution, temp file management, * and process execution — following the adapter pattern in `adapters/tts/`. * * @module adapters/video/frameExtractor */ import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js"; import { logger } from "../../utils/logger.js"; import { cleanupTempFiles, createTrackedTempDir, FFMPEG_FRAME_MAX_BUFFER, FFMPEG_FRAME_TIMEOUT_MS, isValidMp4Buffer, JPEG_QUALITY, join, LAST_FRAME_SEEK_OFFSET, readFile, runFfmpeg, writeFile, } from "./ffmpegAdapter.js"; import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js"; import { VideoError } from "./vertexVideoHandler.js"; // ============================================================================ // INTERNAL HELPERS // ============================================================================ /** * Validate a video buffer before FFmpeg processing. * * @throws {VideoError} If the buffer is empty, too small, or not a valid MP4 */ function assertValidVideoBuffer(videoBuffer, operation) { if (!Buffer.isBuffer(videoBuffer) || videoBuffer.length === 0) { throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_FRAME_EXTRACTION_FAILED, message: `Cannot ${operation}: video buffer is empty or not a Buffer`, category: ErrorCategory.VALIDATION, severity: ErrorSeverity.HIGH, retriable: false, context: { operation, bufferSize: videoBuffer?.length ?? 0 }, }); } if (!isValidMp4Buffer(videoBuffer)) { throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_FRAME_EXTRACTION_FAILED, message: `Cannot ${operation}: buffer does not appear to be a valid MP4 (missing ftyp header)`, category: ErrorCategory.VALIDATION, severity: ErrorSeverity.HIGH, retriable: false, context: { operation, bufferSize: videoBuffer.length, headerHex: videoBuffer.subarray(0, 12).toString("hex"), }, }); } } /** * Core frame extraction logic shared between first/last frame extractors. */ async function extractFrame(videoBuffer, position) { const startTime = Date.now(); assertValidVideoBuffer(videoBuffer, `extract ${position} frame`); const tempDir = await createTrackedTempDir("frame"); const inputPath = join(tempDir, "input.mp4"); const outputPath = join(tempDir, `${position}_frame.jpg`); try { await writeFile(inputPath, videoBuffer); const args = position === "first" ? [ "-y", "-i", inputPath, "-vframes", "1", "-q:v", JPEG_QUALITY, "-f", "image2", outputPath, ] : [ "-y", "-sseof", `-${LAST_FRAME_SEEK_OFFSET}`, "-i", inputPath, "-update", "1", "-q:v", JPEG_QUALITY, "-f", "image2", outputPath, ]; await runFfmpeg(args, { timeoutMs: FFMPEG_FRAME_TIMEOUT_MS, maxBuffer: FFMPEG_FRAME_MAX_BUFFER, }); const frameBuffer = await readFile(outputPath); logger.debug(`Extracted ${position} frame`, { inputSize: videoBuffer.length, frameSize: frameBuffer.length, elapsedMs: Date.now() - startTime, }); return frameBuffer; } catch (error) { // Re-throw VideoErrors as-is if (error instanceof VideoError) { throw error; } throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_FRAME_EXTRACTION_FAILED, message: `Failed to extract ${position} frame: ${error instanceof Error ? error.message : String(error)}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { position, bufferSize: videoBuffer.length }, originalError: error instanceof Error ? error : undefined, }); } finally { await cleanupTempFiles(tempDir, inputPath, outputPath); } } // ============================================================================ // PUBLIC API // ============================================================================ /** * Extract the first frame from a video buffer as JPEG. * * @param videoBuffer - MP4 video buffer * @returns JPEG image buffer of the first frame * @throws {VideoError} If buffer validation or extraction fails */ export async function extractFirstFrame(videoBuffer) { return extractFrame(videoBuffer, "first"); } /** * Extract the last frame from a video buffer as JPEG. * * @param videoBuffer - MP4 video buffer * @returns JPEG image buffer of the last frame * @throws {VideoError} If buffer validation or extraction fails */ export async function extractLastFrame(videoBuffer) { return extractFrame(videoBuffer, "last"); } //# sourceMappingURL=frameExtractor.js.map