vargai 0.3.0 → 0.3.1
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/README.md +38 -1
- package/bun.lock +1 -0
- package/package.json +3 -2
- package/src/ai-sdk/index.ts +21 -2
- package/src/ai-sdk/middleware/index.ts +25 -0
- package/src/ai-sdk/middleware/placeholder.ts +111 -0
- package/src/ai-sdk/middleware/wrap-image-model.ts +86 -0
- package/src/ai-sdk/middleware/wrap-music-model.ts +108 -0
- package/src/ai-sdk/middleware/wrap-video-model.ts +115 -0
- /package/src/ai-sdk/providers/{elevenlabs-provider.ts → elevenlabs.ts} +0 -0
- /package/src/ai-sdk/providers/{fal-provider.ts → fal.ts} +0 -0
package/README.md
CHANGED
|
@@ -5,9 +5,46 @@ AI video generation from your terminal.
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun install
|
|
8
|
+
bun install vargai ai
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
### SDK Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { generateImage } from "ai";
|
|
15
|
+
import { File, fal, generateElement, generateVideo, scene } from "vargai";
|
|
16
|
+
|
|
17
|
+
// generate a character from reference image
|
|
18
|
+
const { element: character } = await generateElement({
|
|
19
|
+
model: fal.imageModel("nano-banana-pro/edit"),
|
|
20
|
+
type: "character",
|
|
21
|
+
prompt: {
|
|
22
|
+
text: "cartoon character, simple style",
|
|
23
|
+
images: [await File.fromPath("media/reference.jpg").arrayBuffer()],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// generate scene with character
|
|
28
|
+
const { image: frame } = await generateImage({
|
|
29
|
+
model: fal.imageModel("nano-banana-pro"),
|
|
30
|
+
prompt: scene`${character} walks through a forest`,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// animate the frame
|
|
34
|
+
const { video } = await generateVideo({
|
|
35
|
+
model: fal.videoModel("wan-2.5"),
|
|
36
|
+
prompt: {
|
|
37
|
+
text: `${character.text} walks through a forest`,
|
|
38
|
+
images: [frame.base64],
|
|
39
|
+
},
|
|
40
|
+
duration: 5,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await Bun.write("output/scene.mp4", video.uint8Array);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CLI Usage
|
|
47
|
+
|
|
11
48
|
```bash
|
|
12
49
|
varg run image --prompt "cyberpunk cityscape at night"
|
|
13
50
|
varg run video --prompt "camera flies through clouds" --duration 5
|
package/bun.lock
CHANGED
package/package.json
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"lint-staged": "^16.2.7"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"typescript": "^5"
|
|
35
|
+
"typescript": "^5",
|
|
36
|
+
"ai": "^6.0.0"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@ai-sdk/fal": "^1.0.23",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"replicate": "^1.4.0",
|
|
60
61
|
"zod": "^4.2.1"
|
|
61
62
|
},
|
|
62
|
-
"version": "0.3.
|
|
63
|
+
"version": "0.3.1",
|
|
63
64
|
"exports": {
|
|
64
65
|
".": "./src/index.ts",
|
|
65
66
|
"./core": "./src/core/index.ts",
|
package/src/ai-sdk/index.ts
CHANGED
|
@@ -25,6 +25,25 @@ export {
|
|
|
25
25
|
type GenerateVideoResult,
|
|
26
26
|
generateVideo,
|
|
27
27
|
} from "./generate-video";
|
|
28
|
+
export {
|
|
29
|
+
generatePlaceholder,
|
|
30
|
+
type ImagePlaceholderFallbackOptions,
|
|
31
|
+
imagePlaceholderFallbackMiddleware,
|
|
32
|
+
type MusicModelMiddleware,
|
|
33
|
+
type MusicPlaceholderFallbackOptions,
|
|
34
|
+
musicPlaceholderFallbackMiddleware,
|
|
35
|
+
type PlaceholderFallbackOptions,
|
|
36
|
+
type PlaceholderOptions,
|
|
37
|
+
type PlaceholderResult,
|
|
38
|
+
placeholderFallbackMiddleware,
|
|
39
|
+
type RenderMode,
|
|
40
|
+
type VideoModelMiddleware,
|
|
41
|
+
withImagePlaceholderFallback,
|
|
42
|
+
withMusicPlaceholderFallback,
|
|
43
|
+
withPlaceholderFallback,
|
|
44
|
+
wrapMusicModel,
|
|
45
|
+
wrapVideoModel,
|
|
46
|
+
} from "./middleware";
|
|
28
47
|
export type {
|
|
29
48
|
MusicModelV3,
|
|
30
49
|
MusicModelV3CallOptions,
|
|
@@ -42,8 +61,8 @@ export {
|
|
|
42
61
|
type ElevenLabsProvider,
|
|
43
62
|
elevenlabs,
|
|
44
63
|
VOICES,
|
|
45
|
-
} from "./providers/elevenlabs
|
|
46
|
-
export { createFal, type FalProvider, fal } from "./providers/fal
|
|
64
|
+
} from "./providers/elevenlabs";
|
|
65
|
+
export { createFal, type FalProvider, fal } from "./providers/fal";
|
|
47
66
|
export {
|
|
48
67
|
createHiggsfield,
|
|
49
68
|
type HiggsfieldImageModelSettings,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export {
|
|
2
|
+
generatePlaceholder,
|
|
3
|
+
type PlaceholderOptions,
|
|
4
|
+
type PlaceholderResult,
|
|
5
|
+
} from "./placeholder";
|
|
6
|
+
export {
|
|
7
|
+
type ImagePlaceholderFallbackOptions,
|
|
8
|
+
imagePlaceholderFallbackMiddleware,
|
|
9
|
+
withImagePlaceholderFallback,
|
|
10
|
+
} from "./wrap-image-model";
|
|
11
|
+
export {
|
|
12
|
+
type MusicModelMiddleware,
|
|
13
|
+
type MusicPlaceholderFallbackOptions,
|
|
14
|
+
musicPlaceholderFallbackMiddleware,
|
|
15
|
+
withMusicPlaceholderFallback,
|
|
16
|
+
wrapMusicModel,
|
|
17
|
+
} from "./wrap-music-model";
|
|
18
|
+
export {
|
|
19
|
+
type PlaceholderFallbackOptions,
|
|
20
|
+
placeholderFallbackMiddleware,
|
|
21
|
+
type RenderMode,
|
|
22
|
+
type VideoModelMiddleware,
|
|
23
|
+
withPlaceholderFallback,
|
|
24
|
+
wrapVideoModel,
|
|
25
|
+
} from "./wrap-video-model";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { $ } from "bun";
|
|
5
|
+
|
|
6
|
+
export interface PlaceholderOptions {
|
|
7
|
+
type: "image" | "video" | "audio";
|
|
8
|
+
prompt: string;
|
|
9
|
+
duration?: number;
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PlaceholderResult {
|
|
15
|
+
data: Uint8Array;
|
|
16
|
+
placeholder: true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function promptToColor(prompt: string): string {
|
|
20
|
+
let hash = 0;
|
|
21
|
+
for (let i = 0; i < prompt.length; i++) {
|
|
22
|
+
const char = prompt.charCodeAt(i);
|
|
23
|
+
hash = (hash << 5) - hash + char;
|
|
24
|
+
hash = hash & hash;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const h = Math.abs(hash) % 360;
|
|
28
|
+
const s = 40 + (Math.abs(hash >> 8) % 30);
|
|
29
|
+
const l = 35 + (Math.abs(hash >> 16) % 20);
|
|
30
|
+
|
|
31
|
+
return `hsl(${h},${s}%,${l}%)`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hslToHex(hsl: string): string {
|
|
35
|
+
const match = hsl.match(/hsl\((\d+),(\d+)%,(\d+)%\)/);
|
|
36
|
+
if (!match) return "333333";
|
|
37
|
+
|
|
38
|
+
const h = Number.parseInt(match[1]!) / 360;
|
|
39
|
+
const s = Number.parseInt(match[2]!) / 100;
|
|
40
|
+
const l = Number.parseInt(match[3]!) / 100;
|
|
41
|
+
|
|
42
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
43
|
+
if (t < 0) t += 1;
|
|
44
|
+
if (t > 1) t -= 1;
|
|
45
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
46
|
+
if (t < 1 / 2) return q;
|
|
47
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
48
|
+
return p;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
52
|
+
const p = 2 * l - q;
|
|
53
|
+
const r = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
|
|
54
|
+
const g = Math.round(hue2rgb(p, q, h) * 255);
|
|
55
|
+
const b = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
|
|
56
|
+
|
|
57
|
+
return `${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function truncatePrompt(text: string, maxLen: number): string {
|
|
61
|
+
const clean = text.replace(/[^a-zA-Z0-9 .,!?-]/g, "");
|
|
62
|
+
if (clean.length <= maxLen) return clean;
|
|
63
|
+
return `${clean.slice(0, maxLen - 3)}...`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function generatePlaceholder(
|
|
67
|
+
options: PlaceholderOptions,
|
|
68
|
+
): Promise<PlaceholderResult> {
|
|
69
|
+
const { type, prompt, duration = 3, width = 1080, height = 1920 } = options;
|
|
70
|
+
|
|
71
|
+
const color = promptToColor(prompt);
|
|
72
|
+
const hexColor = hslToHex(color);
|
|
73
|
+
const labelFontSize = Math.floor(Math.min(width, height) / 20);
|
|
74
|
+
const promptFontSize = Math.floor(Math.min(width, height) / 35);
|
|
75
|
+
const maxChars = Math.floor((width * 0.7) / (promptFontSize * 0.5));
|
|
76
|
+
const typeLabel = type.toUpperCase();
|
|
77
|
+
const promptText = truncatePrompt(prompt, maxChars);
|
|
78
|
+
|
|
79
|
+
const ext = type === "audio" ? "mp3" : type === "image" ? "png" : "mp4";
|
|
80
|
+
const outputPath = join(
|
|
81
|
+
tmpdir(),
|
|
82
|
+
`placeholder_${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (type === "audio") {
|
|
87
|
+
await $`ffmpeg -y -f lavfi -i anullsrc=r=44100:cl=stereo -t ${duration} -c:a libmp3lame ${outputPath}`.quiet();
|
|
88
|
+
} else if (type === "image") {
|
|
89
|
+
const colorInput = `color=c=0x${hexColor}:s=${width}x${height}:d=1`;
|
|
90
|
+
const labelY = `(h/2)-${labelFontSize}`;
|
|
91
|
+
const promptY = `(h/2)+${Math.floor(labelFontSize * 0.5)}`;
|
|
92
|
+
const drawLabel = `drawtext=text='${typeLabel}':fontcolor=white:fontsize=${labelFontSize}:x=(w-text_w)/2:y=${labelY}`;
|
|
93
|
+
const drawPrompt = `drawtext=text='${promptText}':fontcolor=white@0.7:fontsize=${promptFontSize}:x=(w-text_w)/2:y=${promptY}`;
|
|
94
|
+
await $`ffmpeg -y -f lavfi -i ${colorInput} -vf ${drawLabel},${drawPrompt} -frames:v 1 -update 1 ${outputPath}`.quiet();
|
|
95
|
+
} else {
|
|
96
|
+
const colorInput = `color=c=0x${hexColor}:s=${width}x${height}:d=${duration}:r=30`;
|
|
97
|
+
const labelY = `(h/2)-${labelFontSize}`;
|
|
98
|
+
const promptY = `(h/2)+${Math.floor(labelFontSize * 0.5)}`;
|
|
99
|
+
const drawLabel = `drawtext=text='${typeLabel}':fontcolor=white:fontsize=${labelFontSize}:x=(w-text_w)/2:y=${labelY}`;
|
|
100
|
+
const drawPrompt = `drawtext=text='${promptText}':fontcolor=white@0.7:fontsize=${promptFontSize}:x=(w-text_w)/2:y=${promptY}`;
|
|
101
|
+
await $`ffmpeg -y -f lavfi -i ${colorInput} -vf ${drawLabel},${drawPrompt} -c:v libx264 -preset ultrafast -pix_fmt yuv420p ${outputPath}`.quiet();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await Bun.file(outputPath).bytes();
|
|
105
|
+
await unlink(outputPath).catch(() => {});
|
|
106
|
+
return { data: new Uint8Array(data), placeholder: true };
|
|
107
|
+
} catch (e) {
|
|
108
|
+
await unlink(outputPath).catch(() => {});
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ImageModelV3,
|
|
3
|
+
ImageModelV3CallOptions,
|
|
4
|
+
ImageModelV3Middleware,
|
|
5
|
+
} from "@ai-sdk/provider";
|
|
6
|
+
import { wrapImageModel } from "ai";
|
|
7
|
+
import { generatePlaceholder } from "./placeholder";
|
|
8
|
+
import type { RenderMode } from "./wrap-video-model";
|
|
9
|
+
|
|
10
|
+
export interface ImagePlaceholderFallbackOptions {
|
|
11
|
+
mode: RenderMode;
|
|
12
|
+
onFallback?: (error: Error, prompt: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function imagePlaceholderFallbackMiddleware(
|
|
16
|
+
options: ImagePlaceholderFallbackOptions,
|
|
17
|
+
): ImageModelV3Middleware {
|
|
18
|
+
const { mode, onFallback } = options;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
specificationVersion: "v3",
|
|
22
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
23
|
+
const createPlaceholderResult = async () => {
|
|
24
|
+
const [width, height] = (params.size?.split("x").map(Number) ?? [
|
|
25
|
+
1024, 1024,
|
|
26
|
+
]) as [number, number];
|
|
27
|
+
const prompt =
|
|
28
|
+
typeof params.prompt === "string"
|
|
29
|
+
? params.prompt
|
|
30
|
+
: ((params.prompt as { text?: string } | undefined)?.text ??
|
|
31
|
+
"placeholder");
|
|
32
|
+
|
|
33
|
+
const placeholder = await generatePlaceholder({
|
|
34
|
+
type: "image",
|
|
35
|
+
prompt,
|
|
36
|
+
width,
|
|
37
|
+
height,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
images: [placeholder.data],
|
|
42
|
+
warnings: [
|
|
43
|
+
{
|
|
44
|
+
type: "other" as const,
|
|
45
|
+
message: "placeholder: provider skipped or failed",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
response: {
|
|
49
|
+
timestamp: new Date(),
|
|
50
|
+
modelId: model.modelId,
|
|
51
|
+
headers: undefined,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (mode === "preview") {
|
|
57
|
+
return createPlaceholderResult();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
return await doGenerate();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (mode === "strict") throw e;
|
|
64
|
+
|
|
65
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
66
|
+
const promptText =
|
|
67
|
+
typeof params.prompt === "string"
|
|
68
|
+
? params.prompt
|
|
69
|
+
: ((params.prompt as { text?: string } | undefined)?.text ??
|
|
70
|
+
"placeholder");
|
|
71
|
+
onFallback?.(error, promptText);
|
|
72
|
+
return createPlaceholderResult();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function withImagePlaceholderFallback(
|
|
79
|
+
model: ImageModelV3,
|
|
80
|
+
options: ImagePlaceholderFallbackOptions,
|
|
81
|
+
): ImageModelV3 {
|
|
82
|
+
return wrapImageModel({
|
|
83
|
+
model,
|
|
84
|
+
middleware: imagePlaceholderFallbackMiddleware(options),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { MusicModelV3, MusicModelV3CallOptions } from "../music-model";
|
|
2
|
+
import { generatePlaceholder } from "./placeholder";
|
|
3
|
+
import type { RenderMode } from "./wrap-video-model";
|
|
4
|
+
|
|
5
|
+
export interface MusicModelMiddleware {
|
|
6
|
+
transformParams?: (options: {
|
|
7
|
+
params: MusicModelV3CallOptions;
|
|
8
|
+
model: MusicModelV3;
|
|
9
|
+
}) => PromiseLike<MusicModelV3CallOptions> | MusicModelV3CallOptions;
|
|
10
|
+
|
|
11
|
+
wrapGenerate?: (options: {
|
|
12
|
+
doGenerate: () => PromiseLike<
|
|
13
|
+
Awaited<ReturnType<MusicModelV3["doGenerate"]>>
|
|
14
|
+
>;
|
|
15
|
+
params: MusicModelV3CallOptions;
|
|
16
|
+
model: MusicModelV3;
|
|
17
|
+
}) => PromiseLike<Awaited<ReturnType<MusicModelV3["doGenerate"]>>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function wrapMusicModel({
|
|
21
|
+
model,
|
|
22
|
+
middleware,
|
|
23
|
+
}: {
|
|
24
|
+
model: MusicModelV3;
|
|
25
|
+
middleware: MusicModelMiddleware;
|
|
26
|
+
}): MusicModelV3 {
|
|
27
|
+
const { transformParams, wrapGenerate } = middleware;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
specificationVersion: "v3",
|
|
31
|
+
provider: model.provider,
|
|
32
|
+
modelId: model.modelId,
|
|
33
|
+
|
|
34
|
+
async doGenerate(params: MusicModelV3CallOptions) {
|
|
35
|
+
const transformedParams = transformParams
|
|
36
|
+
? await transformParams({ params, model })
|
|
37
|
+
: params;
|
|
38
|
+
|
|
39
|
+
const doGenerate = () => model.doGenerate(transformedParams);
|
|
40
|
+
|
|
41
|
+
return wrapGenerate
|
|
42
|
+
? wrapGenerate({ doGenerate, params: transformedParams, model })
|
|
43
|
+
: doGenerate();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MusicPlaceholderFallbackOptions {
|
|
49
|
+
mode: RenderMode;
|
|
50
|
+
onFallback?: (error: Error, prompt: string) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function musicPlaceholderFallbackMiddleware(
|
|
54
|
+
options: MusicPlaceholderFallbackOptions,
|
|
55
|
+
): MusicModelMiddleware {
|
|
56
|
+
const { mode, onFallback } = options;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
60
|
+
const createPlaceholderResult = async () => {
|
|
61
|
+
const placeholder = await generatePlaceholder({
|
|
62
|
+
type: "audio",
|
|
63
|
+
prompt: params.prompt,
|
|
64
|
+
duration: params.duration ?? 10,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
audio: placeholder.data,
|
|
69
|
+
warnings: [
|
|
70
|
+
{
|
|
71
|
+
type: "other" as const,
|
|
72
|
+
message: "placeholder: provider skipped or failed",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
response: {
|
|
76
|
+
timestamp: new Date(),
|
|
77
|
+
modelId: model.modelId,
|
|
78
|
+
headers: undefined,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (mode === "preview") {
|
|
84
|
+
return createPlaceholderResult();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return await doGenerate();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (mode === "strict") throw e;
|
|
91
|
+
|
|
92
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
93
|
+
onFallback?.(error, params.prompt);
|
|
94
|
+
return createPlaceholderResult();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function withMusicPlaceholderFallback(
|
|
101
|
+
model: MusicModelV3,
|
|
102
|
+
options: MusicPlaceholderFallbackOptions,
|
|
103
|
+
): MusicModelV3 {
|
|
104
|
+
return wrapMusicModel({
|
|
105
|
+
model,
|
|
106
|
+
middleware: musicPlaceholderFallbackMiddleware(options),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { VideoModelV3, VideoModelV3CallOptions } from "../video-model";
|
|
2
|
+
import { generatePlaceholder } from "./placeholder";
|
|
3
|
+
|
|
4
|
+
export type RenderMode = "strict" | "default" | "preview";
|
|
5
|
+
|
|
6
|
+
export interface VideoModelMiddleware {
|
|
7
|
+
transformParams?: (options: {
|
|
8
|
+
params: VideoModelV3CallOptions;
|
|
9
|
+
model: VideoModelV3;
|
|
10
|
+
}) => PromiseLike<VideoModelV3CallOptions> | VideoModelV3CallOptions;
|
|
11
|
+
|
|
12
|
+
wrapGenerate?: (options: {
|
|
13
|
+
doGenerate: () => PromiseLike<
|
|
14
|
+
Awaited<ReturnType<VideoModelV3["doGenerate"]>>
|
|
15
|
+
>;
|
|
16
|
+
params: VideoModelV3CallOptions;
|
|
17
|
+
model: VideoModelV3;
|
|
18
|
+
}) => PromiseLike<Awaited<ReturnType<VideoModelV3["doGenerate"]>>>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function wrapVideoModel({
|
|
22
|
+
model,
|
|
23
|
+
middleware,
|
|
24
|
+
}: {
|
|
25
|
+
model: VideoModelV3;
|
|
26
|
+
middleware: VideoModelMiddleware;
|
|
27
|
+
}): VideoModelV3 {
|
|
28
|
+
const { transformParams, wrapGenerate } = middleware;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
specificationVersion: "v3",
|
|
32
|
+
provider: model.provider,
|
|
33
|
+
modelId: model.modelId,
|
|
34
|
+
maxVideosPerCall: model.maxVideosPerCall,
|
|
35
|
+
|
|
36
|
+
async doGenerate(params: VideoModelV3CallOptions) {
|
|
37
|
+
const transformedParams = transformParams
|
|
38
|
+
? await transformParams({ params, model })
|
|
39
|
+
: params;
|
|
40
|
+
|
|
41
|
+
const doGenerate = () => model.doGenerate(transformedParams);
|
|
42
|
+
|
|
43
|
+
return wrapGenerate
|
|
44
|
+
? wrapGenerate({ doGenerate, params: transformedParams, model })
|
|
45
|
+
: doGenerate();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PlaceholderFallbackOptions {
|
|
51
|
+
mode: RenderMode;
|
|
52
|
+
onFallback?: (error: Error, prompt: string) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function placeholderFallbackMiddleware(
|
|
56
|
+
options: PlaceholderFallbackOptions,
|
|
57
|
+
): VideoModelMiddleware {
|
|
58
|
+
const { mode, onFallback } = options;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
62
|
+
const createPlaceholderResult = async () => {
|
|
63
|
+
const [width, height] = (params.resolution?.split("x").map(Number) ?? [
|
|
64
|
+
1080, 1920,
|
|
65
|
+
]) as [number, number];
|
|
66
|
+
const placeholder = await generatePlaceholder({
|
|
67
|
+
type: "video",
|
|
68
|
+
prompt: params.prompt,
|
|
69
|
+
duration: params.duration ?? 3,
|
|
70
|
+
width,
|
|
71
|
+
height,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
videos: [placeholder.data],
|
|
76
|
+
warnings: [
|
|
77
|
+
{
|
|
78
|
+
type: "other" as const,
|
|
79
|
+
message: "placeholder: provider skipped or failed",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
response: {
|
|
83
|
+
timestamp: new Date(),
|
|
84
|
+
modelId: model.modelId,
|
|
85
|
+
headers: undefined,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (mode === "preview") {
|
|
91
|
+
return createPlaceholderResult();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
return await doGenerate();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (mode === "strict") throw e;
|
|
98
|
+
|
|
99
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
100
|
+
onFallback?.(error, params.prompt);
|
|
101
|
+
return createPlaceholderResult();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function withPlaceholderFallback(
|
|
108
|
+
model: VideoModelV3,
|
|
109
|
+
options: PlaceholderFallbackOptions,
|
|
110
|
+
): VideoModelV3 {
|
|
111
|
+
return wrapVideoModel({
|
|
112
|
+
model,
|
|
113
|
+
middleware: placeholderFallbackMiddleware(options),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
File without changes
|
|
File without changes
|