@juspay/neurolink 9.22.2 → 9.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/adapters/video/directorPipeline.d.ts +31 -0
- package/dist/adapters/video/directorPipeline.js +516 -0
- package/dist/adapters/video/ffmpegAdapter.d.ts +78 -0
- package/dist/adapters/video/ffmpegAdapter.js +206 -0
- package/dist/adapters/video/frameExtractor.d.ts +28 -0
- package/dist/adapters/video/frameExtractor.js +143 -0
- package/dist/adapters/video/vertexVideoHandler.d.ts +25 -25
- package/dist/adapters/video/vertexVideoHandler.js +173 -42
- package/dist/adapters/video/videoMerger.d.ts +22 -0
- package/dist/adapters/video/videoMerger.js +171 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +2 -0
- package/dist/constants/videoErrors.d.ts +45 -0
- package/dist/constants/videoErrors.js +46 -0
- package/dist/core/baseProvider.js +42 -1
- package/dist/lib/adapters/video/directorPipeline.d.ts +31 -0
- package/dist/lib/adapters/video/directorPipeline.js +517 -0
- package/dist/lib/adapters/video/ffmpegAdapter.d.ts +78 -0
- package/dist/lib/adapters/video/ffmpegAdapter.js +207 -0
- package/dist/lib/adapters/video/frameExtractor.d.ts +28 -0
- package/dist/lib/adapters/video/frameExtractor.js +144 -0
- package/dist/lib/adapters/video/vertexVideoHandler.d.ts +25 -25
- package/dist/lib/adapters/video/vertexVideoHandler.js +173 -42
- package/dist/lib/adapters/video/videoMerger.d.ts +22 -0
- package/dist/lib/adapters/video/videoMerger.js +172 -0
- package/dist/lib/constants/index.d.ts +1 -0
- package/dist/lib/constants/index.js +2 -0
- package/dist/lib/constants/videoErrors.d.ts +45 -0
- package/dist/lib/constants/videoErrors.js +47 -0
- package/dist/lib/core/baseProvider.js +42 -1
- package/dist/lib/types/content.d.ts +1 -1
- package/dist/lib/types/generateTypes.d.ts +18 -1
- package/dist/lib/types/multimodal.d.ts +64 -4
- package/dist/lib/types/multimodal.js +36 -1
- package/dist/lib/utils/parameterValidation.d.ts +8 -1
- package/dist/lib/utils/parameterValidation.js +80 -1
- package/dist/types/content.d.ts +1 -1
- package/dist/types/generateTypes.d.ts +18 -1
- package/dist/types/multimodal.d.ts +64 -4
- package/dist/types/multimodal.js +36 -1
- package/dist/utils/parameterValidation.d.ts +8 -1
- package/dist/utils/parameterValidation.js +80 -1
- package/package.json +1 -1
|
@@ -12,38 +12,17 @@
|
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
13
|
import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
|
|
14
14
|
import { TIMEOUTS } from "../../constants/timeouts.js";
|
|
15
|
+
import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js";
|
|
15
16
|
import { isAbortError, NeuroLinkError, withTimeout, } from "../../utils/errorHandling.js";
|
|
16
17
|
import { logger } from "../../utils/logger.js";
|
|
17
18
|
// ============================================================================
|
|
18
|
-
// VIDEO ERROR CODES
|
|
19
|
+
// VIDEO ERROR CODES (Re-exported for backward compatibility)
|
|
19
20
|
// ============================================================================
|
|
20
21
|
/**
|
|
21
|
-
* Video
|
|
22
|
-
*
|
|
23
|
-
* These are for runtime/execution errors during video generation.
|
|
24
|
-
* Pure option/shape validation (missing image option, invalid config values, etc.)
|
|
25
|
-
* is handled by parameterValidation.ts using ERROR_CODES from errorHandling.ts.
|
|
26
|
-
*
|
|
27
|
-
* Error categorization:
|
|
28
|
-
* - INVALID_INPUT → ErrorCategory.execution (runtime I/O failures)
|
|
29
|
-
* - parameterValidation errors → ErrorCategory.validation (schema/option issues)
|
|
30
|
-
*
|
|
31
|
-
* Following TTS pattern (TTS_ERROR_CODES + TTSError in ttsProcessor.ts)
|
|
22
|
+
* Video error codes - re-exported from constants module for backward compatibility.
|
|
23
|
+
* @see {@link VIDEO_ERROR_CODES} in constants/videoErrors.ts for definitions
|
|
32
24
|
*/
|
|
33
|
-
export
|
|
34
|
-
/** Video generation API call failed */
|
|
35
|
-
GENERATION_FAILED: "VIDEO_GENERATION_FAILED",
|
|
36
|
-
/** Provider (Vertex AI) not properly configured */
|
|
37
|
-
PROVIDER_NOT_CONFIGURED: "VIDEO_PROVIDER_NOT_CONFIGURED",
|
|
38
|
-
/** Polling for video completion timed out */
|
|
39
|
-
POLL_TIMEOUT: "VIDEO_POLL_TIMEOUT",
|
|
40
|
-
/**
|
|
41
|
-
* Runtime I/O error during input processing.
|
|
42
|
-
* Used for: failed URL fetch, failed file read, corrupt/unreadable buffer.
|
|
43
|
-
* NOT for: missing options or invalid config shapes (use parameterValidation).
|
|
44
|
-
*/
|
|
45
|
-
INVALID_INPUT: "VIDEO_INVALID_INPUT",
|
|
46
|
-
};
|
|
25
|
+
export { VIDEO_ERROR_CODES };
|
|
47
26
|
/**
|
|
48
27
|
* Video generation error class
|
|
49
28
|
* Extends NeuroLinkError for consistent error handling across the SDK
|
|
@@ -71,6 +50,8 @@ const VIDEO_GENERATION_TIMEOUT_MS = 180000;
|
|
|
71
50
|
const POLL_INTERVAL_MS = 5000;
|
|
72
51
|
/** Full model name for Veo 3.1 (IMPORTANT: not just "veo-3.1") */
|
|
73
52
|
const VEO_MODEL = "veo-3.1-generate-001";
|
|
53
|
+
/** Full model name for Veo 3.1 Fast (used for transitions) */
|
|
54
|
+
const VEO_FAST_MODEL = "veo-3.1-fast-generate-001";
|
|
74
55
|
/** Default location for Vertex AI */
|
|
75
56
|
const DEFAULT_LOCATION = "us-central1";
|
|
76
57
|
// ============================================================================
|
|
@@ -592,35 +573,185 @@ async function makePollRequest(pollEndpoint, operationName, accessToken, timeout
|
|
|
592
573
|
* @throws {VideoError} On API error, timeout, or missing video data
|
|
593
574
|
*/
|
|
594
575
|
async function pollVideoOperation(operationName, accessToken, project, location, timeoutMs) {
|
|
576
|
+
return pollOperation(VEO_MODEL, operationName, accessToken, project, location, timeoutMs);
|
|
577
|
+
}
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// TRANSITION GENERATION (Director Mode)
|
|
580
|
+
// ============================================================================
|
|
581
|
+
/**
|
|
582
|
+
* Generate a transition clip using Veo 3.1 Fast's first-and-last-frame interpolation.
|
|
583
|
+
*
|
|
584
|
+
* This calls the Veo API with both `image` (first frame) and `lastFrame` (last frame),
|
|
585
|
+
* producing a video that smoothly interpolates between the two frames.
|
|
586
|
+
*
|
|
587
|
+
* @param firstFrame - JPEG buffer of the first frame (last frame of clip N)
|
|
588
|
+
* @param lastFrame - JPEG buffer of the last frame (first frame of clip N+1)
|
|
589
|
+
* @param prompt - Transition prompt describing desired visual flow
|
|
590
|
+
* @param options - Video output options (resolution, aspect ratio, audio)
|
|
591
|
+
* @param durationSeconds - Duration of the transition clip (4, 6, or 8)
|
|
592
|
+
* @param region - Vertex AI region override
|
|
593
|
+
* @returns Video buffer of the transition clip
|
|
594
|
+
*
|
|
595
|
+
* @throws {VideoError} When API returns an error or polling times out
|
|
596
|
+
*/
|
|
597
|
+
export async function generateTransitionWithVertex(firstFrame, lastFrame, prompt, options = {}, durationSeconds = 4, region) {
|
|
598
|
+
if (!isVertexVideoConfigured()) {
|
|
599
|
+
throw new VideoError({
|
|
600
|
+
code: VIDEO_ERROR_CODES.PROVIDER_NOT_CONFIGURED,
|
|
601
|
+
message: "Vertex AI credentials not configured for transition generation.",
|
|
602
|
+
category: ErrorCategory.CONFIGURATION,
|
|
603
|
+
severity: ErrorSeverity.HIGH,
|
|
604
|
+
retriable: false,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const config = await getVertexConfig();
|
|
608
|
+
const project = config.project;
|
|
609
|
+
const location = region || config.location;
|
|
595
610
|
const startTime = Date.now();
|
|
596
|
-
|
|
597
|
-
const
|
|
611
|
+
const aspectRatio = options.aspectRatio || "16:9";
|
|
612
|
+
const resolution = options.resolution || "720p";
|
|
613
|
+
const generateAudio = options.audio ?? true;
|
|
614
|
+
logger.debug("Starting transition clip generation", {
|
|
615
|
+
model: VEO_FAST_MODEL,
|
|
616
|
+
durationSeconds,
|
|
617
|
+
firstFrameSize: firstFrame.length,
|
|
618
|
+
lastFrameSize: lastFrame.length,
|
|
619
|
+
promptLength: prompt.length,
|
|
620
|
+
});
|
|
621
|
+
try {
|
|
622
|
+
const firstFrameBase64 = firstFrame.toString("base64");
|
|
623
|
+
const lastFrameBase64 = lastFrame.toString("base64");
|
|
624
|
+
const firstMime = detectMimeType(firstFrame);
|
|
625
|
+
const lastMime = detectMimeType(lastFrame);
|
|
626
|
+
const accessToken = await getAccessToken();
|
|
627
|
+
// Use Veo 3.1 Fast for transitions (faster with minimal quality difference)
|
|
628
|
+
const endpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${VEO_FAST_MODEL}:predictLongRunning`;
|
|
629
|
+
const requestBody = {
|
|
630
|
+
instances: [
|
|
631
|
+
{
|
|
632
|
+
prompt: prompt,
|
|
633
|
+
image: {
|
|
634
|
+
bytesBase64Encoded: firstFrameBase64,
|
|
635
|
+
mimeType: firstMime,
|
|
636
|
+
},
|
|
637
|
+
lastFrame: {
|
|
638
|
+
bytesBase64Encoded: lastFrameBase64,
|
|
639
|
+
mimeType: lastMime,
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
parameters: {
|
|
644
|
+
sampleCount: 1,
|
|
645
|
+
durationSeconds: durationSeconds,
|
|
646
|
+
aspectRatio: aspectRatio,
|
|
647
|
+
resolution: resolution,
|
|
648
|
+
generateAudio: generateAudio,
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
const controller = new AbortController();
|
|
652
|
+
const requestTimeout = setTimeout(() => controller.abort(), 30000);
|
|
653
|
+
let response;
|
|
654
|
+
try {
|
|
655
|
+
response = await fetch(endpoint, {
|
|
656
|
+
method: "POST",
|
|
657
|
+
headers: {
|
|
658
|
+
Authorization: `Bearer ${accessToken}`,
|
|
659
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
660
|
+
},
|
|
661
|
+
body: JSON.stringify(requestBody),
|
|
662
|
+
signal: controller.signal,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
clearTimeout(requestTimeout);
|
|
667
|
+
if (isAbortError(error)) {
|
|
668
|
+
throw new VideoError({
|
|
669
|
+
code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED,
|
|
670
|
+
message: "Transition generation request timed out after 30 seconds",
|
|
671
|
+
category: ErrorCategory.EXECUTION,
|
|
672
|
+
severity: ErrorSeverity.MEDIUM,
|
|
673
|
+
retriable: true,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
clearTimeout(requestTimeout);
|
|
679
|
+
if (!response.ok) {
|
|
680
|
+
const errorText = await response.text();
|
|
681
|
+
throw new VideoError({
|
|
682
|
+
code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED,
|
|
683
|
+
message: `Transition API error: ${response.status} - ${errorText}`,
|
|
684
|
+
category: ErrorCategory.EXECUTION,
|
|
685
|
+
severity: ErrorSeverity.MEDIUM,
|
|
686
|
+
retriable: response.status >= 500,
|
|
687
|
+
context: { status: response.status, error: errorText },
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
const operation = await response.json();
|
|
691
|
+
const operationName = operation.name;
|
|
692
|
+
if (!operationName) {
|
|
693
|
+
throw new VideoError({
|
|
694
|
+
code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED,
|
|
695
|
+
message: "Transition API did not return an operation name",
|
|
696
|
+
category: ErrorCategory.EXECUTION,
|
|
697
|
+
severity: ErrorSeverity.MEDIUM,
|
|
698
|
+
retriable: false,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
// Poll with Veo Fast model endpoint
|
|
702
|
+
const remainingTime = VIDEO_GENERATION_TIMEOUT_MS - (Date.now() - startTime);
|
|
703
|
+
const videoBuffer = await pollTransitionOperation(operationName, accessToken, project, location, Math.max(1000, remainingTime));
|
|
704
|
+
logger.debug("Transition clip generated", {
|
|
705
|
+
processingTime: Date.now() - startTime,
|
|
706
|
+
videoSize: videoBuffer.length,
|
|
707
|
+
});
|
|
708
|
+
return videoBuffer;
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
if (error instanceof VideoError) {
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
throw new VideoError({
|
|
715
|
+
code: VIDEO_ERROR_CODES.DIRECTOR_TRANSITION_FAILED,
|
|
716
|
+
message: `Transition generation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
717
|
+
category: ErrorCategory.EXECUTION,
|
|
718
|
+
severity: ErrorSeverity.MEDIUM,
|
|
719
|
+
retriable: true,
|
|
720
|
+
originalError: error instanceof Error ? error : undefined,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Common polling helper that handles both video and transition operations.
|
|
726
|
+
* Accepts a model name to construct the appropriate endpoint.
|
|
727
|
+
*/
|
|
728
|
+
async function pollOperation(modelOrEndpoint, operationName, accessToken, project, location, timeoutMs) {
|
|
729
|
+
const startTime = Date.now();
|
|
730
|
+
const pollEndpoint = `https://${location}-aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/publishers/google/models/${modelOrEndpoint}:fetchPredictOperation`;
|
|
598
731
|
while (Date.now() - startTime < timeoutMs) {
|
|
599
732
|
const result = await makePollRequest(pollEndpoint, operationName, accessToken);
|
|
600
733
|
if (result.done) {
|
|
601
734
|
return extractVideoFromResult(result, operationName);
|
|
602
735
|
}
|
|
603
|
-
|
|
604
|
-
logger.debug("Polling video operation...", {
|
|
736
|
+
logger.debug("Polling operation...", {
|
|
605
737
|
operationName,
|
|
606
|
-
elapsed,
|
|
607
|
-
remainingMs: timeoutMs - elapsed,
|
|
738
|
+
elapsed: Date.now() - startTime,
|
|
608
739
|
});
|
|
609
|
-
// Wait before next poll
|
|
610
740
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
611
741
|
}
|
|
612
|
-
// Timeout reached
|
|
613
742
|
throw new VideoError({
|
|
614
743
|
code: VIDEO_ERROR_CODES.POLL_TIMEOUT,
|
|
615
|
-
message: `
|
|
744
|
+
message: `Operation timed out after ${Math.round(timeoutMs / 1000)}s`,
|
|
616
745
|
category: ErrorCategory.TIMEOUT,
|
|
617
|
-
severity: ErrorSeverity.
|
|
746
|
+
severity: ErrorSeverity.MEDIUM,
|
|
618
747
|
retriable: true,
|
|
619
|
-
context: {
|
|
620
|
-
operationName,
|
|
621
|
-
timeoutMs,
|
|
622
|
-
provider: "vertex",
|
|
623
|
-
suggestion: "Try again - video generation can take 1-3 minutes. Consider using a shorter duration or lower resolution.",
|
|
624
|
-
},
|
|
748
|
+
context: { operationName, timeoutMs },
|
|
625
749
|
});
|
|
626
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* Poll Vertex AI operation for transition clip completion.
|
|
753
|
+
* Uses the Veo Fast model fetchPredictOperation endpoint.
|
|
754
|
+
*/
|
|
755
|
+
async function pollTransitionOperation(operationName, accessToken, project, location, timeoutMs) {
|
|
756
|
+
return pollOperation(VEO_FAST_MODEL, operationName, accessToken, project, location, timeoutMs);
|
|
757
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Merger for Director Mode
|
|
3
|
+
*
|
|
4
|
+
* Concatenates multiple MP4 video buffers into a single MP4 using FFmpeg's
|
|
5
|
+
* concat demuxer for lossless concatenation when codecs match.
|
|
6
|
+
*
|
|
7
|
+
* Uses the shared FFmpeg adapter for binary resolution, temp file management,
|
|
8
|
+
* and process execution — following the adapter pattern in `adapters/tts/`.
|
|
9
|
+
*
|
|
10
|
+
* @module adapters/video/videoMerger
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Merge multiple MP4 video buffers into a single MP4.
|
|
14
|
+
*
|
|
15
|
+
* Uses FFmpeg concat demuxer for lossless concatenation. If codecs don't match
|
|
16
|
+
* (unlikely since all clips come from Veo), falls back to re-encoding with H.264.
|
|
17
|
+
*
|
|
18
|
+
* @param videoBuffers - Array of MP4 video buffers to concatenate in order
|
|
19
|
+
* @returns Merged MP4 video buffer
|
|
20
|
+
* @throws {VideoError} If merge fails or no valid buffers provided
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergeVideoBuffers(videoBuffers: Buffer[]): Promise<Buffer>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Merger for Director Mode
|
|
3
|
+
*
|
|
4
|
+
* Concatenates multiple MP4 video buffers into a single MP4 using FFmpeg's
|
|
5
|
+
* concat demuxer for lossless concatenation when codecs match.
|
|
6
|
+
*
|
|
7
|
+
* Uses the shared FFmpeg adapter for binary resolution, temp file management,
|
|
8
|
+
* and process execution — following the adapter pattern in `adapters/tts/`.
|
|
9
|
+
*
|
|
10
|
+
* @module adapters/video/videoMerger
|
|
11
|
+
*/
|
|
12
|
+
import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
|
|
13
|
+
import { logger } from "../../utils/logger.js";
|
|
14
|
+
import { cleanupTempFiles, createTrackedTempDir, FFMPEG_MERGE_MAX_BUFFER, FFMPEG_MERGE_TIMEOUT_MS, isValidMp4Buffer, join, readFile, runFfmpeg, writeFile, } from "./ffmpegAdapter.js";
|
|
15
|
+
import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js";
|
|
16
|
+
import { VideoError } from "./vertexVideoHandler.js";
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// INTERNAL HELPERS
|
|
19
|
+
// ============================================================================
|
|
20
|
+
/**
|
|
21
|
+
* Write clip buffers to temp files and build the FFmpeg concat list.
|
|
22
|
+
*
|
|
23
|
+
* @returns Paths of all written clip files
|
|
24
|
+
*/
|
|
25
|
+
async function writeClipsAndBuildConcatList(videoBuffers, tempDir, concatListPath) {
|
|
26
|
+
const inputPaths = [];
|
|
27
|
+
const concatLines = [];
|
|
28
|
+
for (let i = 0; i < videoBuffers.length; i++) {
|
|
29
|
+
const inputPath = join(tempDir, `clip_${i}.mp4`);
|
|
30
|
+
await writeFile(inputPath, videoBuffers[i]);
|
|
31
|
+
inputPaths.push(inputPath);
|
|
32
|
+
// Normalize backslashes to forward slashes for FFmpeg, then escape single quotes
|
|
33
|
+
const safePath = inputPath.replace(/\\/g, "/").replace(/'/g, "'\\''");
|
|
34
|
+
concatLines.push(`file '${safePath}'`);
|
|
35
|
+
}
|
|
36
|
+
await writeFile(concatListPath, concatLines.join("\n"));
|
|
37
|
+
return inputPaths;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Attempt lossless concat, falling back to H.264 re-encode on failure.
|
|
41
|
+
*/
|
|
42
|
+
async function concatWithFallback(concatListPath, outputPath) {
|
|
43
|
+
const ffmpegOpts = {
|
|
44
|
+
timeoutMs: FFMPEG_MERGE_TIMEOUT_MS,
|
|
45
|
+
maxBuffer: FFMPEG_MERGE_MAX_BUFFER,
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
// Try lossless concat first (fastest — no re-encoding)
|
|
49
|
+
await runFfmpeg([
|
|
50
|
+
"-y",
|
|
51
|
+
"-f",
|
|
52
|
+
"concat",
|
|
53
|
+
"-safe",
|
|
54
|
+
"0",
|
|
55
|
+
"-i",
|
|
56
|
+
concatListPath,
|
|
57
|
+
"-c",
|
|
58
|
+
"copy",
|
|
59
|
+
outputPath,
|
|
60
|
+
], ffmpegOpts);
|
|
61
|
+
}
|
|
62
|
+
catch (concatError) {
|
|
63
|
+
// Fallback: re-encode with H.264 if codecs mismatch
|
|
64
|
+
logger.warn("Lossless concat failed, falling back to H.264 re-encoding", {
|
|
65
|
+
error: concatError instanceof Error
|
|
66
|
+
? concatError.message
|
|
67
|
+
: String(concatError),
|
|
68
|
+
});
|
|
69
|
+
await runFfmpeg([
|
|
70
|
+
"-y",
|
|
71
|
+
"-f",
|
|
72
|
+
"concat",
|
|
73
|
+
"-safe",
|
|
74
|
+
"0",
|
|
75
|
+
"-i",
|
|
76
|
+
concatListPath,
|
|
77
|
+
"-c:v",
|
|
78
|
+
"libx264",
|
|
79
|
+
"-preset",
|
|
80
|
+
"fast",
|
|
81
|
+
"-crf",
|
|
82
|
+
"18",
|
|
83
|
+
"-c:a",
|
|
84
|
+
"aac",
|
|
85
|
+
"-b:a",
|
|
86
|
+
"192k",
|
|
87
|
+
"-movflags",
|
|
88
|
+
"+faststart",
|
|
89
|
+
outputPath,
|
|
90
|
+
], ffmpegOpts);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// PUBLIC API
|
|
95
|
+
// ============================================================================
|
|
96
|
+
/**
|
|
97
|
+
* Merge multiple MP4 video buffers into a single MP4.
|
|
98
|
+
*
|
|
99
|
+
* Uses FFmpeg concat demuxer for lossless concatenation. If codecs don't match
|
|
100
|
+
* (unlikely since all clips come from Veo), falls back to re-encoding with H.264.
|
|
101
|
+
*
|
|
102
|
+
* @param videoBuffers - Array of MP4 video buffers to concatenate in order
|
|
103
|
+
* @returns Merged MP4 video buffer
|
|
104
|
+
* @throws {VideoError} If merge fails or no valid buffers provided
|
|
105
|
+
*/
|
|
106
|
+
export async function mergeVideoBuffers(videoBuffers) {
|
|
107
|
+
if (videoBuffers.length === 0) {
|
|
108
|
+
throw new VideoError({
|
|
109
|
+
code: VIDEO_ERROR_CODES.DIRECTOR_MERGE_FAILED,
|
|
110
|
+
message: "No video buffers provided to merge",
|
|
111
|
+
category: ErrorCategory.VALIDATION,
|
|
112
|
+
severity: ErrorSeverity.HIGH,
|
|
113
|
+
retriable: false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Validate each input buffer
|
|
117
|
+
for (let i = 0; i < videoBuffers.length; i++) {
|
|
118
|
+
if (!isValidMp4Buffer(videoBuffers[i])) {
|
|
119
|
+
throw new VideoError({
|
|
120
|
+
code: VIDEO_ERROR_CODES.DIRECTOR_MERGE_FAILED,
|
|
121
|
+
message: `Clip ${i} is not a valid MP4 buffer (missing ftyp header or too small)`,
|
|
122
|
+
category: ErrorCategory.VALIDATION,
|
|
123
|
+
severity: ErrorSeverity.HIGH,
|
|
124
|
+
retriable: false,
|
|
125
|
+
context: {
|
|
126
|
+
clipIndex: i,
|
|
127
|
+
bufferSize: videoBuffers[i].length,
|
|
128
|
+
headerHex: videoBuffers[i].subarray(0, 12).toString("hex"),
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (videoBuffers.length === 1) {
|
|
134
|
+
return videoBuffers[0];
|
|
135
|
+
}
|
|
136
|
+
const startTime = Date.now();
|
|
137
|
+
const tempDir = await createTrackedTempDir("merge");
|
|
138
|
+
const concatListPath = join(tempDir, "concat.txt");
|
|
139
|
+
const outputPath = join(tempDir, "merged.mp4");
|
|
140
|
+
let inputPaths = [];
|
|
141
|
+
try {
|
|
142
|
+
inputPaths = await writeClipsAndBuildConcatList(videoBuffers, tempDir, concatListPath);
|
|
143
|
+
await concatWithFallback(concatListPath, outputPath);
|
|
144
|
+
const mergedBuffer = await readFile(outputPath);
|
|
145
|
+
logger.info("Video merge complete", {
|
|
146
|
+
inputClips: videoBuffers.length,
|
|
147
|
+
totalInputSize: videoBuffers.reduce((sum, b) => sum + b.length, 0),
|
|
148
|
+
outputSize: mergedBuffer.length,
|
|
149
|
+
elapsedMs: Date.now() - startTime,
|
|
150
|
+
});
|
|
151
|
+
return mergedBuffer;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// Re-throw VideoErrors as-is
|
|
155
|
+
if (error instanceof VideoError) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
throw new VideoError({
|
|
159
|
+
code: VIDEO_ERROR_CODES.DIRECTOR_MERGE_FAILED,
|
|
160
|
+
message: `Video merge failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
161
|
+
category: ErrorCategory.EXECUTION,
|
|
162
|
+
severity: ErrorSeverity.HIGH,
|
|
163
|
+
retriable: false,
|
|
164
|
+
context: { clipCount: videoBuffers.length },
|
|
165
|
+
originalError: error instanceof Error ? error : undefined,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await cleanupTempFiles(tempDir, concatListPath, outputPath, ...inputPaths);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -191,4 +191,5 @@ export declare const CONSTANTS_METADATA: {
|
|
|
191
191
|
readonly COMPATIBILITY: "backward_compatible";
|
|
192
192
|
};
|
|
193
193
|
export { AIProviderName, OpenRouterModels, BedrockModels, OpenAIModels, AzureOpenAIModels, VertexModels, GoogleAIModels, AnthropicModels, MistralModels, OllamaModels, LiteLLMModels, HuggingFaceModels, SageMakerModels, APIVersions, ErrorCategory, ErrorSeverity, AnthropicBetaFeature, TOKEN_EXPIRY_BUFFER_MS, } from "./enums.js";
|
|
194
|
+
export { VIDEO_ERROR_CODES } from "./videoErrors.js";
|
|
194
195
|
export type { ClaudeSubscriptionTier, AnthropicAuthMethod, } from "../types/subscriptionTypes.js";
|
package/dist/constants/index.js
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation Error Codes
|
|
3
|
+
*
|
|
4
|
+
* Centralized error codes for video generation operations.
|
|
5
|
+
* These are for runtime/execution errors during video generation.
|
|
6
|
+
*
|
|
7
|
+
* Pure option/shape validation (missing image option, invalid config values, etc.)
|
|
8
|
+
* is handled by parameterValidation.ts using ERROR_CODES from errorHandling.ts.
|
|
9
|
+
*
|
|
10
|
+
* Error categorization:
|
|
11
|
+
* - INVALID_INPUT → ErrorCategory.execution (runtime I/O failures)
|
|
12
|
+
* - parameterValidation errors → ErrorCategory.validation (schema/option issues)
|
|
13
|
+
*
|
|
14
|
+
* @module constants/videoErrors
|
|
15
|
+
*/
|
|
16
|
+
export declare const VIDEO_ERROR_CODES: {
|
|
17
|
+
/** Video generation API call failed */
|
|
18
|
+
readonly GENERATION_FAILED: "VIDEO_GENERATION_FAILED";
|
|
19
|
+
/** Provider (Vertex AI) not properly configured */
|
|
20
|
+
readonly PROVIDER_NOT_CONFIGURED: "VIDEO_PROVIDER_NOT_CONFIGURED";
|
|
21
|
+
/** Polling for video completion timed out */
|
|
22
|
+
readonly POLL_TIMEOUT: "VIDEO_POLL_TIMEOUT";
|
|
23
|
+
/**
|
|
24
|
+
* Runtime I/O error during input processing.
|
|
25
|
+
* Used for: failed URL fetch, failed file read, corrupt/unreadable buffer.
|
|
26
|
+
* NOT for: missing options or invalid config shapes (use parameterValidation).
|
|
27
|
+
*/
|
|
28
|
+
readonly INVALID_INPUT: "VIDEO_INVALID_INPUT";
|
|
29
|
+
/** Invalid segment structure (missing prompt or image) */
|
|
30
|
+
readonly DIRECTOR_SEGMENT_MISMATCH: "DIRECTOR_SEGMENT_MISMATCH";
|
|
31
|
+
/** Too many segments requested */
|
|
32
|
+
readonly DIRECTOR_SEGMENT_LIMIT_EXCEEDED: "DIRECTOR_SEGMENT_LIMIT_EXCEEDED";
|
|
33
|
+
/** Invalid transition duration (must be 4, 6, or 8) */
|
|
34
|
+
readonly DIRECTOR_INVALID_TRANSITION_DURATION: "DIRECTOR_INVALID_TRANSITION_DURATION";
|
|
35
|
+
/** A main clip generation call failed (fatal) */
|
|
36
|
+
readonly DIRECTOR_CLIP_FAILED: "DIRECTOR_CLIP_FAILED";
|
|
37
|
+
/** Frame extraction from clip failed */
|
|
38
|
+
readonly DIRECTOR_FRAME_EXTRACTION_FAILED: "DIRECTOR_FRAME_EXTRACTION_FAILED";
|
|
39
|
+
/** Transition clip generation failed (non-fatal, falls back to hard cut) */
|
|
40
|
+
readonly DIRECTOR_TRANSITION_FAILED: "DIRECTOR_TRANSITION_FAILED";
|
|
41
|
+
/** Video merge/concatenation failed */
|
|
42
|
+
readonly DIRECTOR_MERGE_FAILED: "DIRECTOR_MERGE_FAILED";
|
|
43
|
+
/** Pipeline timeout (overall) */
|
|
44
|
+
readonly DIRECTOR_PIPELINE_TIMEOUT: "DIRECTOR_PIPELINE_TIMEOUT";
|
|
45
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation Error Codes
|
|
3
|
+
*
|
|
4
|
+
* Centralized error codes for video generation operations.
|
|
5
|
+
* These are for runtime/execution errors during video generation.
|
|
6
|
+
*
|
|
7
|
+
* Pure option/shape validation (missing image option, invalid config values, etc.)
|
|
8
|
+
* is handled by parameterValidation.ts using ERROR_CODES from errorHandling.ts.
|
|
9
|
+
*
|
|
10
|
+
* Error categorization:
|
|
11
|
+
* - INVALID_INPUT → ErrorCategory.execution (runtime I/O failures)
|
|
12
|
+
* - parameterValidation errors → ErrorCategory.validation (schema/option issues)
|
|
13
|
+
*
|
|
14
|
+
* @module constants/videoErrors
|
|
15
|
+
*/
|
|
16
|
+
export const VIDEO_ERROR_CODES = {
|
|
17
|
+
/** Video generation API call failed */
|
|
18
|
+
GENERATION_FAILED: "VIDEO_GENERATION_FAILED",
|
|
19
|
+
/** Provider (Vertex AI) not properly configured */
|
|
20
|
+
PROVIDER_NOT_CONFIGURED: "VIDEO_PROVIDER_NOT_CONFIGURED",
|
|
21
|
+
/** Polling for video completion timed out */
|
|
22
|
+
POLL_TIMEOUT: "VIDEO_POLL_TIMEOUT",
|
|
23
|
+
/**
|
|
24
|
+
* Runtime I/O error during input processing.
|
|
25
|
+
* Used for: failed URL fetch, failed file read, corrupt/unreadable buffer.
|
|
26
|
+
* NOT for: missing options or invalid config shapes (use parameterValidation).
|
|
27
|
+
*/
|
|
28
|
+
INVALID_INPUT: "VIDEO_INVALID_INPUT",
|
|
29
|
+
// Director Mode error codes
|
|
30
|
+
/** Invalid segment structure (missing prompt or image) */
|
|
31
|
+
DIRECTOR_SEGMENT_MISMATCH: "DIRECTOR_SEGMENT_MISMATCH",
|
|
32
|
+
/** Too many segments requested */
|
|
33
|
+
DIRECTOR_SEGMENT_LIMIT_EXCEEDED: "DIRECTOR_SEGMENT_LIMIT_EXCEEDED",
|
|
34
|
+
/** Invalid transition duration (must be 4, 6, or 8) */
|
|
35
|
+
DIRECTOR_INVALID_TRANSITION_DURATION: "DIRECTOR_INVALID_TRANSITION_DURATION",
|
|
36
|
+
/** A main clip generation call failed (fatal) */
|
|
37
|
+
DIRECTOR_CLIP_FAILED: "DIRECTOR_CLIP_FAILED",
|
|
38
|
+
/** Frame extraction from clip failed */
|
|
39
|
+
DIRECTOR_FRAME_EXTRACTION_FAILED: "DIRECTOR_FRAME_EXTRACTION_FAILED",
|
|
40
|
+
/** Transition clip generation failed (non-fatal, falls back to hard cut) */
|
|
41
|
+
DIRECTOR_TRANSITION_FAILED: "DIRECTOR_TRANSITION_FAILED",
|
|
42
|
+
/** Video merge/concatenation failed */
|
|
43
|
+
DIRECTOR_MERGE_FAILED: "DIRECTOR_MERGE_FAILED",
|
|
44
|
+
/** Pipeline timeout (overall) */
|
|
45
|
+
DIRECTOR_PIPELINE_TIMEOUT: "DIRECTOR_PIPELINE_TIMEOUT",
|
|
46
|
+
};
|
|
@@ -1055,7 +1055,7 @@ export class BaseProvider {
|
|
|
1055
1055
|
async handleVideoGeneration(options, startTime) {
|
|
1056
1056
|
// Dynamic imports to avoid loading video dependencies unless needed
|
|
1057
1057
|
const { generateVideoWithVertex, VideoError, VIDEO_ERROR_CODES } = await import("../adapters/video/vertexVideoHandler.js");
|
|
1058
|
-
const { validateVideoGenerationInput, validateImageForVideo } = await import("../utils/parameterValidation.js");
|
|
1058
|
+
const { validateVideoGenerationInput, validateImageForVideo, validateDirectorModeInput, } = await import("../utils/parameterValidation.js");
|
|
1059
1059
|
const { ErrorFactory } = await import("../utils/errorHandling.js");
|
|
1060
1060
|
// Build GenerateOptions for validation
|
|
1061
1061
|
const generateOptions = {
|
|
@@ -1064,6 +1064,47 @@ export class BaseProvider {
|
|
|
1064
1064
|
provider: options.provider,
|
|
1065
1065
|
model: options.model,
|
|
1066
1066
|
};
|
|
1067
|
+
// ===== DIRECTOR MODE =====
|
|
1068
|
+
// Route to Director pipeline when segments are provided
|
|
1069
|
+
if (generateOptions.input?.segments &&
|
|
1070
|
+
Array.isArray(generateOptions.input.segments) &&
|
|
1071
|
+
generateOptions.input.segments.length > 0) {
|
|
1072
|
+
// Type narrowing: segments is guaranteed to exist here
|
|
1073
|
+
const segments = generateOptions.input.segments;
|
|
1074
|
+
const directorValidation = validateDirectorModeInput(generateOptions);
|
|
1075
|
+
if (!directorValidation.isValid) {
|
|
1076
|
+
throw ErrorFactory.invalidParameters("director-mode", new Error(directorValidation.errors
|
|
1077
|
+
.map((e) => e.message)
|
|
1078
|
+
.join("; ")), { errors: directorValidation.errors });
|
|
1079
|
+
}
|
|
1080
|
+
if (directorValidation.warnings.length > 0) {
|
|
1081
|
+
for (const warning of directorValidation.warnings) {
|
|
1082
|
+
logger.warn(`Director Mode warning: ${warning}`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const { executeDirectorPipeline, DIRECTOR_PIPELINE_TIMEOUT_MS } = await import("../adapters/video/directorPipeline.js");
|
|
1086
|
+
// Use caller's timeout if provided, otherwise use default Director timeout
|
|
1087
|
+
const directorTimeout = options.timeout ?? DIRECTOR_PIPELINE_TIMEOUT_MS;
|
|
1088
|
+
const videoResult = await this.executeWithTimeout(() => executeDirectorPipeline(segments, generateOptions.output?.video ?? {}, generateOptions.output?.director ?? {}, options.region), { timeout: directorTimeout, operationType: "generate" });
|
|
1089
|
+
// Build content summary with metadata
|
|
1090
|
+
const joinedPrompts = generateOptions.input.segments
|
|
1091
|
+
.map((s) => s.prompt)
|
|
1092
|
+
.join(" → ");
|
|
1093
|
+
const segmentCount = videoResult.metadata?.segmentCount ??
|
|
1094
|
+
generateOptions.input.segments.length;
|
|
1095
|
+
const transitionCount = videoResult.metadata?.transitionCount ?? Math.max(0, segmentCount - 1);
|
|
1096
|
+
const totalDuration = videoResult.metadata?.duration ?? 0;
|
|
1097
|
+
const contentSummary = `${joinedPrompts} — duration: ${totalDuration}s, segments: ${segmentCount}, transitions: ${transitionCount}`;
|
|
1098
|
+
const baseResult = {
|
|
1099
|
+
content: contentSummary,
|
|
1100
|
+
provider: "vertex",
|
|
1101
|
+
model: options.model || "veo-3.1-generate-001",
|
|
1102
|
+
usage: { input: 0, output: 0, total: 0 },
|
|
1103
|
+
video: videoResult,
|
|
1104
|
+
};
|
|
1105
|
+
return await this.enhanceResult(baseResult, options, startTime);
|
|
1106
|
+
}
|
|
1107
|
+
// ===== STANDARD SINGLE-CLIP VIDEO GENERATION =====
|
|
1067
1108
|
// Validate video generation input
|
|
1068
1109
|
const validation = validateVideoGenerationInput(generateOptions);
|
|
1069
1110
|
if (!validation.isValid) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Director Mode Pipeline Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates multi-segment video generation: parallel clip generation,
|
|
5
|
+
* parallel frame extraction + transition generation, and sequential merge.
|
|
6
|
+
*
|
|
7
|
+
* Error severity semantics:
|
|
8
|
+
* - HIGH: Fatal — clip generation / merge failures that abort the pipeline
|
|
9
|
+
* - MEDIUM: Non-fatal — transition / frame-extraction failures that degrade
|
|
10
|
+
* to a hard cut but do not abort the pipeline
|
|
11
|
+
*
|
|
12
|
+
* @module adapters/video/directorPipeline
|
|
13
|
+
*/
|
|
14
|
+
import type { DirectorModeOptions, DirectorSegment, VideoGenerationResult, VideoOutputOptions } from "../../types/multimodal.js";
|
|
15
|
+
/** Default timeout for entire Director Mode pipeline (10 minutes) */
|
|
16
|
+
export declare const DIRECTOR_PIPELINE_TIMEOUT_MS = 600000;
|
|
17
|
+
/**
|
|
18
|
+
* Execute the full Director Mode pipeline.
|
|
19
|
+
*
|
|
20
|
+
* Pipeline stages:
|
|
21
|
+
* 1. Parallel clip generation (concurrency = 2, circuit breaker after 2 failures)
|
|
22
|
+
* 2. Parallel frame extraction + transition generation
|
|
23
|
+
* 3. Sequential merge into single MP4
|
|
24
|
+
*
|
|
25
|
+
* @param segments - Array of DirectorSegment objects (2-10)
|
|
26
|
+
* @param videoOptions - Video output options (resolution, length, aspectRatio, audio)
|
|
27
|
+
* @param directorOptions - Director Mode options (transition prompts/durations)
|
|
28
|
+
* @param region - Vertex AI region override
|
|
29
|
+
* @returns VideoGenerationResult with merged video and Director metadata
|
|
30
|
+
*/
|
|
31
|
+
export declare function executeDirectorPipeline(segments: DirectorSegment[], videoOptions?: VideoOutputOptions, directorOptions?: DirectorModeOptions, region?: string): Promise<VideoGenerationResult>;
|