vargai 0.4.0-alpha4 → 0.4.0-alpha40
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/.env.example +6 -0
- package/README.md +483 -61
- package/assets/fonts/TikTokSans-Bold.ttf +0 -0
- package/examples/grok-imagine-test.tsx +155 -0
- package/launch-videos/06-kawaii-fruits.tsx +93 -0
- package/launch-videos/07-ugc-weight-loss.tsx +132 -0
- package/launch-videos/08-talking-head-varg.tsx +107 -0
- package/launch-videos/09-girl.tsx +160 -0
- package/launch-videos/README.md +42 -0
- package/package.json +10 -4
- package/pipeline/cookbooks/round-video-character.md +1 -1
- package/skills/varg-video-generation/SKILL.md +224 -0
- package/skills/varg-video-generation/references/templates.md +380 -0
- package/skills/varg-video-generation/scripts/setup.ts +265 -0
- package/src/ai-sdk/cache.ts +1 -3
- package/src/ai-sdk/examples/google-image.ts +62 -0
- package/src/ai-sdk/index.ts +10 -0
- package/src/ai-sdk/middleware/wrap-image-model.ts +4 -21
- package/src/ai-sdk/middleware/wrap-music-model.ts +4 -16
- package/src/ai-sdk/middleware/wrap-video-model.ts +5 -17
- package/src/ai-sdk/providers/CONTRIBUTING.md +457 -0
- package/src/ai-sdk/providers/editly/backends/index.ts +8 -0
- package/src/ai-sdk/providers/editly/backends/local.ts +94 -0
- package/src/ai-sdk/providers/editly/backends/types.ts +74 -0
- package/src/ai-sdk/providers/editly/editly.test.ts +49 -1
- package/src/ai-sdk/providers/editly/index.ts +164 -80
- package/src/ai-sdk/providers/editly/layers.ts +58 -6
- package/src/ai-sdk/providers/editly/rendi/editly-with-rendi-backend.test.ts +335 -0
- package/src/ai-sdk/providers/editly/rendi/index.ts +289 -0
- package/src/ai-sdk/providers/editly/rendi/rendi.test.ts +35 -0
- package/src/ai-sdk/providers/editly/types.ts +30 -0
- package/src/ai-sdk/providers/elevenlabs.ts +10 -2
- package/src/ai-sdk/providers/fal.test.ts +214 -0
- package/src/ai-sdk/providers/fal.ts +435 -40
- package/src/ai-sdk/providers/google.ts +423 -0
- package/src/ai-sdk/providers/together.ts +191 -0
- package/src/cli/commands/find.tsx +1 -0
- package/src/cli/commands/frame.tsx +616 -0
- package/src/cli/commands/hello.ts +85 -0
- package/src/cli/commands/help.tsx +18 -30
- package/src/cli/commands/index.ts +11 -2
- package/src/cli/commands/init.tsx +570 -0
- package/src/cli/commands/list.tsx +1 -0
- package/src/cli/commands/render.tsx +322 -76
- package/src/cli/commands/run.tsx +1 -0
- package/src/cli/commands/storyboard.tsx +1714 -0
- package/src/cli/commands/which.tsx +1 -0
- package/src/cli/index.ts +23 -4
- package/src/cli/ui/components/Badge.tsx +1 -0
- package/src/cli/ui/components/DataTable.tsx +1 -0
- package/src/cli/ui/components/Header.tsx +1 -0
- package/src/cli/ui/components/HelpBlock.tsx +1 -0
- package/src/cli/ui/components/KeyValue.tsx +1 -0
- package/src/cli/ui/components/OptionRow.tsx +1 -0
- package/src/cli/ui/components/Separator.tsx +1 -0
- package/src/cli/ui/components/StatusBox.tsx +1 -0
- package/src/cli/ui/components/VargBox.tsx +1 -0
- package/src/cli/ui/components/VargProgress.tsx +1 -0
- package/src/cli/ui/components/VargSpinner.tsx +1 -0
- package/src/cli/ui/components/VargText.tsx +1 -0
- package/src/definitions/actions/grok-edit.ts +133 -0
- package/src/definitions/actions/index.ts +16 -0
- package/src/definitions/actions/qwen-angles.ts +218 -0
- package/src/index.ts +1 -0
- package/src/providers/fal.ts +196 -0
- package/src/react/assets.ts +9 -0
- package/src/react/elements.ts +0 -5
- package/src/react/examples/branching.tsx +6 -4
- package/src/react/examples/character-video.tsx +13 -10
- package/src/react/examples/local-files-test.tsx +19 -0
- package/src/react/examples/ltx2-test.tsx +25 -0
- package/src/react/examples/madi.tsx +13 -10
- package/src/react/examples/mcmeows.tsx +40 -0
- package/src/react/examples/music-defaults.tsx +24 -0
- package/src/react/examples/quickstart-test.tsx +101 -0
- package/src/react/examples/qwen-angles-test.tsx +72 -0
- package/src/react/index.ts +3 -3
- package/src/react/layouts/grid.tsx +1 -1
- package/src/react/layouts/index.ts +2 -1
- package/src/react/layouts/slot.tsx +85 -0
- package/src/react/layouts/split.tsx +18 -0
- package/src/react/react.test.ts +60 -11
- package/src/react/renderers/burn-captions.ts +95 -0
- package/src/react/renderers/cache.test.ts +182 -0
- package/src/react/renderers/captions.ts +25 -6
- package/src/react/renderers/clip.ts +56 -25
- package/src/react/renderers/context.ts +5 -2
- package/src/react/renderers/image.ts +5 -2
- package/src/react/renderers/index.ts +0 -1
- package/src/react/renderers/music.ts +8 -3
- package/src/react/renderers/packshot/blinking-button.ts +413 -0
- package/src/react/renderers/packshot.ts +170 -8
- package/src/react/renderers/progress.ts +4 -3
- package/src/react/renderers/render.ts +127 -71
- package/src/react/renderers/speech.ts +2 -2
- package/src/react/renderers/split.ts +34 -13
- package/src/react/renderers/utils.test.ts +80 -0
- package/src/react/renderers/utils.ts +37 -1
- package/src/react/renderers/video.ts +47 -9
- package/src/react/types.ts +70 -17
- package/src/studio/stages.ts +40 -39
- package/src/studio/step-renderer.ts +14 -24
- package/src/studio/ui/index.html +2 -2
- package/src/tests/all.test.ts +4 -4
- package/src/tests/index.ts +1 -1
- package/test-slot-grid.tsx +19 -0
- package/test-slot-userland.tsx +30 -0
- package/test-sync-v2.ts +30 -0
- package/test-sync-v2.tsx +29 -0
- package/tsconfig.json +1 -1
- package/video.tsx +7 -0
- package/src/ai-sdk/providers/editly/ffmpeg.ts +0 -60
- package/src/react/renderers/animate.ts +0 -59
- /package/src/cli/commands/{studio.tsx → studio.ts} +0 -0
|
@@ -4,14 +4,86 @@ import type {
|
|
|
4
4
|
ImageOverlayLayer,
|
|
5
5
|
Layer,
|
|
6
6
|
Position,
|
|
7
|
+
PositionObject,
|
|
8
|
+
SizeValue,
|
|
7
9
|
TitleLayer,
|
|
8
10
|
} from "../../ai-sdk/providers/editly/types";
|
|
9
11
|
import type { PackshotProps, VargElement } from "../types";
|
|
10
12
|
import type { RenderContext } from "./context";
|
|
11
13
|
import { renderImage } from "./image";
|
|
14
|
+
import { createBlinkingButton } from "./packshot/blinking-button";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Type guard: returns true if `pos` is a PositionObject ({ x, y }).
|
|
18
|
+
*/
|
|
19
|
+
function isPositionObject(pos: Position): pos is PositionObject {
|
|
20
|
+
return typeof pos === "object" && pos !== null && "x" in pos && "y" in pos;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse a SizeValue to a normalised 0-1 fraction.
|
|
25
|
+
*
|
|
26
|
+
* - `number` – treated as a raw pixel value; divided by `total`.
|
|
27
|
+
* - `"50%"` – percentage string; divided by 100.
|
|
28
|
+
* - `"120px"` – pixel string; parsed and divided by `total`.
|
|
29
|
+
*
|
|
30
|
+
* Returns `0.5` (centre) when the value cannot be parsed.
|
|
31
|
+
*/
|
|
32
|
+
function sizeValueToFraction(value: SizeValue, total: number): number {
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
return total > 0 ? value / total : 0.5;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
if (value.endsWith("%")) {
|
|
38
|
+
const n = parseFloat(value);
|
|
39
|
+
return Number.isFinite(n) ? n / 100 : 0.5;
|
|
40
|
+
}
|
|
41
|
+
if (value.endsWith("px")) {
|
|
42
|
+
const n = parseFloat(value);
|
|
43
|
+
return Number.isFinite(n) && total > 0 ? n / total : 0.5;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return 0.5;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert a PositionObject to the nearest string Position.
|
|
51
|
+
*
|
|
52
|
+
* The x axis is split into thirds: left (< 0.33), center, right (> 0.67).
|
|
53
|
+
* The y axis is split into thirds: top (< 0.33), center, bottom (> 0.67).
|
|
54
|
+
*
|
|
55
|
+
* `refWidth` / `refHeight` are only needed when the SizeValue is in pixels;
|
|
56
|
+
* when unknown, pass 1 and only percentage / fraction values will resolve
|
|
57
|
+
* correctly.
|
|
58
|
+
*/
|
|
59
|
+
function positionObjectToString(
|
|
60
|
+
obj: PositionObject,
|
|
61
|
+
refWidth = 1,
|
|
62
|
+
refHeight = 1,
|
|
63
|
+
): Exclude<Position, PositionObject> {
|
|
64
|
+
const fx = sizeValueToFraction(obj.x, refWidth);
|
|
65
|
+
const fy = sizeValueToFraction(obj.y, refHeight);
|
|
66
|
+
|
|
67
|
+
const col: "left" | "center" | "right" =
|
|
68
|
+
fx < 0.33 ? "left" : fx > 0.67 ? "right" : "center";
|
|
69
|
+
const row: "top" | "center" | "bottom" =
|
|
70
|
+
fy < 0.33 ? "top" : fy > 0.67 ? "bottom" : "center";
|
|
71
|
+
|
|
72
|
+
if (row === "center" && col === "center") return "center";
|
|
73
|
+
if (row === "center")
|
|
74
|
+
return `center-${col}` as "center-left" | "center-right";
|
|
75
|
+
if (col === "center") return row; // "top" | "bottom"
|
|
76
|
+
return `${row}-${col}` as
|
|
77
|
+
| "top-left"
|
|
78
|
+
| "top-right"
|
|
79
|
+
| "bottom-left"
|
|
80
|
+
| "bottom-right";
|
|
81
|
+
}
|
|
12
82
|
|
|
13
83
|
function resolvePosition(pos: Position | undefined): Position {
|
|
14
|
-
|
|
84
|
+
if (pos === undefined) return "center";
|
|
85
|
+
if (isPositionObject(pos)) return positionObjectToString(pos);
|
|
86
|
+
return pos;
|
|
15
87
|
}
|
|
16
88
|
|
|
17
89
|
export async function renderPackshot(
|
|
@@ -23,6 +95,7 @@ export async function renderPackshot(
|
|
|
23
95
|
|
|
24
96
|
const layers: Layer[] = [];
|
|
25
97
|
|
|
98
|
+
// ===== BACKGROUND LAYER =====
|
|
26
99
|
if (props.background) {
|
|
27
100
|
if (typeof props.background === "string") {
|
|
28
101
|
layers.push({
|
|
@@ -44,17 +117,30 @@ export async function renderPackshot(
|
|
|
44
117
|
});
|
|
45
118
|
}
|
|
46
119
|
|
|
120
|
+
// ===== LOGO LAYER =====
|
|
47
121
|
if (props.logo) {
|
|
48
122
|
const logoLayer: ImageOverlayLayer = {
|
|
49
123
|
type: "image-overlay",
|
|
50
124
|
path: props.logo,
|
|
51
|
-
position: resolvePosition(props.logoPosition),
|
|
52
|
-
width: props.logoSize ?? "
|
|
125
|
+
position: resolvePosition(props.logoPosition ?? "center"),
|
|
126
|
+
width: props.logoSize ?? "40%",
|
|
53
127
|
};
|
|
54
128
|
layers.push(logoLayer);
|
|
55
129
|
}
|
|
56
130
|
|
|
57
|
-
|
|
131
|
+
// ===== TITLE LAYER =====
|
|
132
|
+
if (props.title) {
|
|
133
|
+
const titleLayer: TitleLayer = {
|
|
134
|
+
type: "title",
|
|
135
|
+
text: props.title,
|
|
136
|
+
textColor: props.titleColor ?? "#FFFFFF",
|
|
137
|
+
position: resolvePosition(props.titlePosition ?? "center"),
|
|
138
|
+
};
|
|
139
|
+
layers.push(titleLayer);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ===== STATIC CTA (non-blinking) =====
|
|
143
|
+
if (props.cta && !props.blinkCta) {
|
|
58
144
|
const ctaLayer: TitleLayer = {
|
|
59
145
|
type: "title",
|
|
60
146
|
text: props.cta,
|
|
@@ -64,21 +150,97 @@ export async function renderPackshot(
|
|
|
64
150
|
layers.push(ctaLayer);
|
|
65
151
|
}
|
|
66
152
|
|
|
153
|
+
// Create base packshot video
|
|
67
154
|
const clip: Clip = {
|
|
68
155
|
layers,
|
|
69
156
|
duration,
|
|
70
157
|
};
|
|
71
158
|
|
|
72
|
-
const
|
|
159
|
+
const basePath = `/tmp/varg-packshot-${Date.now()}.mp4`;
|
|
73
160
|
|
|
74
161
|
await editly({
|
|
75
|
-
outPath,
|
|
162
|
+
outPath: basePath,
|
|
76
163
|
width: ctx.width,
|
|
77
164
|
height: ctx.height,
|
|
78
165
|
fps: ctx.fps,
|
|
79
166
|
clips: [clip],
|
|
80
167
|
});
|
|
81
168
|
|
|
82
|
-
|
|
83
|
-
|
|
169
|
+
// ===== BLINKING CTA OVERLAY =====
|
|
170
|
+
if (props.cta && props.blinkCta) {
|
|
171
|
+
// Create animated button with Sharp at button-size canvas (fast)
|
|
172
|
+
const btn = await createBlinkingButton({
|
|
173
|
+
text: props.cta,
|
|
174
|
+
width: ctx.width,
|
|
175
|
+
height: ctx.height,
|
|
176
|
+
duration,
|
|
177
|
+
fps: ctx.fps,
|
|
178
|
+
bgColor: props.ctaColor ?? "#FF6B00",
|
|
179
|
+
textColor: props.ctaTextColor ?? "#FFFFFF",
|
|
180
|
+
blinkFrequency: props.blinkFrequency ?? 0.8,
|
|
181
|
+
position: mapCtaPosition(props.ctaPosition, ctx.height),
|
|
182
|
+
buttonWidth: props.ctaSize?.width,
|
|
183
|
+
buttonHeight: props.ctaSize?.height,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Composite button-sized overlay at correct position on base video
|
|
187
|
+
const finalPath = `/tmp/varg-packshot-final-${Date.now()}.mp4`;
|
|
188
|
+
const { $ } = await import("bun");
|
|
189
|
+
|
|
190
|
+
// Overlay the blinking button (with alpha) on the packshot
|
|
191
|
+
await $`ffmpeg -y \
|
|
192
|
+
-i ${basePath} \
|
|
193
|
+
-i ${btn.path} \
|
|
194
|
+
-filter_complex "[0:v][1:v]overlay=${btn.x}:${btn.y}:format=auto" \
|
|
195
|
+
-c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p \
|
|
196
|
+
${finalPath}`.quiet();
|
|
197
|
+
|
|
198
|
+
ctx.tempFiles.push(basePath, btn.path);
|
|
199
|
+
return finalPath;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ctx.tempFiles.push(basePath);
|
|
203
|
+
return basePath;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Map a Position (string literal **or** PositionObject) to the vertical
|
|
208
|
+
* bucket that the blinking-button renderer understands.
|
|
209
|
+
*
|
|
210
|
+
* When a PositionObject ({ x, y }) is provided the y-coordinate is
|
|
211
|
+
* normalised to a 0-1 fraction and mapped to "top" (< 0.33),
|
|
212
|
+
* "center" (0.33-0.67), or "bottom" (> 0.67). Pixel values are
|
|
213
|
+
* resolved against `refHeight` (defaults to 1, which means only
|
|
214
|
+
* percentages will convert correctly when the caller does not supply it).
|
|
215
|
+
*/
|
|
216
|
+
function mapCtaPosition(
|
|
217
|
+
pos: Position | undefined,
|
|
218
|
+
refHeight = 1,
|
|
219
|
+
): "top" | "center" | "bottom" {
|
|
220
|
+
if (pos === undefined) return "bottom";
|
|
221
|
+
|
|
222
|
+
// Handle PositionObject ({ x, y }) explicitly
|
|
223
|
+
if (isPositionObject(pos)) {
|
|
224
|
+
const fy = sizeValueToFraction(pos.y, refHeight);
|
|
225
|
+
if (fy < 0.33) return "top";
|
|
226
|
+
if (fy > 0.67) return "bottom";
|
|
227
|
+
return "center";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// String literal positions
|
|
231
|
+
switch (pos) {
|
|
232
|
+
case "top":
|
|
233
|
+
case "top-left":
|
|
234
|
+
case "top-right":
|
|
235
|
+
return "top";
|
|
236
|
+
case "center":
|
|
237
|
+
case "center-left":
|
|
238
|
+
case "center-right":
|
|
239
|
+
return "center";
|
|
240
|
+
case "bottom":
|
|
241
|
+
case "bottom-left":
|
|
242
|
+
case "bottom-right":
|
|
243
|
+
default:
|
|
244
|
+
return "bottom";
|
|
245
|
+
}
|
|
84
246
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export type GenerationType =
|
|
2
2
|
| "image"
|
|
3
3
|
| "video"
|
|
4
|
-
| "animate"
|
|
5
4
|
| "speech"
|
|
6
5
|
| "music"
|
|
7
6
|
| "editly"
|
|
@@ -11,7 +10,6 @@ export type GenerationType =
|
|
|
11
10
|
export const TIME_ESTIMATES: Record<GenerationType, number> = {
|
|
12
11
|
image: 30,
|
|
13
12
|
video: 120,
|
|
14
|
-
animate: 90,
|
|
15
13
|
speech: 5,
|
|
16
14
|
music: 45,
|
|
17
15
|
editly: 15,
|
|
@@ -30,6 +28,9 @@ export const MODEL_TIME_ESTIMATES: Record<string, number> = {
|
|
|
30
28
|
kling: 180,
|
|
31
29
|
"kling-v2": 180,
|
|
32
30
|
"kling-v2.5": 180,
|
|
31
|
+
"kling-v2.6": 180,
|
|
32
|
+
"kling-v2.6-motion": 240,
|
|
33
|
+
"kling-v2.6-motion-standard": 180,
|
|
33
34
|
minimax: 90,
|
|
34
35
|
luma: 90,
|
|
35
36
|
runway: 45,
|
|
@@ -103,7 +104,7 @@ export function completeTask(tracker: ProgressTracker, id: string): void {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
function getEstimate(task: ProgressTask): number {
|
|
106
|
-
const modelLower = task.model
|
|
107
|
+
const modelLower = task.model?.toLowerCase() ?? "";
|
|
107
108
|
for (const [key, estimate] of Object.entries(MODEL_TIME_ESTIMATES)) {
|
|
108
109
|
if (modelLower.includes(key.toLowerCase())) {
|
|
109
110
|
return estimate;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateImage, wrapImageModel } from "ai";
|
|
2
|
-
import { withCache } from "../../ai-sdk/cache";
|
|
2
|
+
import { type CacheStorage, withCache } from "../../ai-sdk/cache";
|
|
3
3
|
import { fileCache } from "../../ai-sdk/file-cache";
|
|
4
4
|
import { generateVideo } from "../../ai-sdk/generate-video";
|
|
5
5
|
import {
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
Layer,
|
|
15
15
|
VideoLayer,
|
|
16
16
|
} from "../../ai-sdk/providers/editly/types";
|
|
17
|
+
|
|
17
18
|
import type {
|
|
18
19
|
CaptionsProps,
|
|
19
20
|
ClipProps,
|
|
@@ -25,7 +26,7 @@ import type {
|
|
|
25
26
|
SpeechProps,
|
|
26
27
|
VargElement,
|
|
27
28
|
} from "../types";
|
|
28
|
-
import {
|
|
29
|
+
import { burnCaptions } from "./burn-captions";
|
|
29
30
|
import { renderCaptions } from "./captions";
|
|
30
31
|
import { renderClip } from "./clip";
|
|
31
32
|
import type { RenderContext } from "./context";
|
|
@@ -47,6 +48,16 @@ interface RenderedOverlay {
|
|
|
47
48
|
isVideo: boolean;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function resolveCacheStorage(
|
|
52
|
+
cache: string | CacheStorage | undefined,
|
|
53
|
+
): CacheStorage | undefined {
|
|
54
|
+
if (!cache) return undefined;
|
|
55
|
+
if (typeof cache === "string") {
|
|
56
|
+
return fileCache({ dir: cache });
|
|
57
|
+
}
|
|
58
|
+
return cache;
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
export async function renderRoot(
|
|
51
62
|
element: VargElement<"render">,
|
|
52
63
|
options: RenderOptions,
|
|
@@ -54,78 +65,74 @@ export async function renderRoot(
|
|
|
54
65
|
const props = element.props as RenderProps;
|
|
55
66
|
const progress = createProgressTracker(options.quiet ?? false);
|
|
56
67
|
|
|
57
|
-
const mode: RenderMode = options.mode ?? "
|
|
68
|
+
const mode: RenderMode = options.mode ?? "strict";
|
|
58
69
|
const placeholderCount = { images: 0, videos: 0, total: 0 };
|
|
59
70
|
|
|
60
|
-
const onFallback = (error: Error, prompt: string) => {
|
|
61
|
-
if (!options.quiet) {
|
|
62
|
-
console.warn(
|
|
63
|
-
`\x1b[33m⚠ provider failed: ${error.message} → placeholder\x1b[0m`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
71
|
const trackPlaceholder = (type: "image" | "video") => {
|
|
69
72
|
placeholderCount[type === "image" ? "images" : "videos"]++;
|
|
70
73
|
placeholderCount.total++;
|
|
71
74
|
};
|
|
72
75
|
|
|
76
|
+
const cacheStorage = resolveCacheStorage(options.cache);
|
|
77
|
+
|
|
78
|
+
const cachedGenerateImage = cacheStorage
|
|
79
|
+
? withCache(generateImage, { storage: cacheStorage })
|
|
80
|
+
: generateImage;
|
|
81
|
+
|
|
82
|
+
const cachedGenerateVideo = cacheStorage
|
|
83
|
+
? withCache(generateVideo, { storage: cacheStorage })
|
|
84
|
+
: generateVideo;
|
|
85
|
+
|
|
73
86
|
const wrapGenerateImage: typeof generateImage = async (opts) => {
|
|
74
87
|
if (
|
|
75
88
|
typeof opts.model === "string" ||
|
|
76
89
|
opts.model.specificationVersion !== "v3"
|
|
77
90
|
) {
|
|
78
|
-
return
|
|
91
|
+
return cachedGenerateImage(opts);
|
|
79
92
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
if (mode === "preview") {
|
|
95
|
+
trackPlaceholder("image");
|
|
96
|
+
const wrappedModel = wrapImageModel({
|
|
97
|
+
model: opts.model,
|
|
98
|
+
middleware: imagePlaceholderFallbackMiddleware({
|
|
99
|
+
mode: "preview",
|
|
100
|
+
onFallback: () => {},
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
return generateImage({ ...opts, model: wrappedModel });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return cachedGenerateImage(opts);
|
|
93
107
|
};
|
|
94
108
|
|
|
95
109
|
const wrapGenerateVideo: typeof generateVideo = async (opts) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
onFallback(
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return
|
|
110
|
+
if (mode === "preview") {
|
|
111
|
+
trackPlaceholder("video");
|
|
112
|
+
const wrappedModel = wrapVideoModel({
|
|
113
|
+
model: opts.model,
|
|
114
|
+
middleware: placeholderFallbackMiddleware({
|
|
115
|
+
mode: "preview",
|
|
116
|
+
onFallback: () => {},
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
return generateVideo({ ...opts, model: wrappedModel });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return cachedGenerateVideo(opts);
|
|
109
123
|
};
|
|
110
124
|
|
|
111
125
|
const ctx: RenderContext = {
|
|
112
126
|
width: props.width ?? 1920,
|
|
113
127
|
height: props.height ?? 1080,
|
|
114
128
|
fps: props.fps ?? 30,
|
|
115
|
-
cache:
|
|
116
|
-
generateImage:
|
|
117
|
-
|
|
118
|
-
storage: fileCache({ dir: options.cache }),
|
|
119
|
-
})
|
|
120
|
-
: wrapGenerateImage,
|
|
121
|
-
generateVideo: options.cache
|
|
122
|
-
? withCache(wrapGenerateVideo, {
|
|
123
|
-
storage: fileCache({ dir: options.cache }),
|
|
124
|
-
})
|
|
125
|
-
: wrapGenerateVideo,
|
|
129
|
+
cache: cacheStorage,
|
|
130
|
+
generateImage: wrapGenerateImage,
|
|
131
|
+
generateVideo: wrapGenerateVideo,
|
|
126
132
|
tempFiles: [],
|
|
127
133
|
progress,
|
|
128
134
|
pending: new Map(),
|
|
135
|
+
defaults: options.defaults,
|
|
129
136
|
};
|
|
130
137
|
|
|
131
138
|
const clipElements: VargElement<"clip">[] = [];
|
|
@@ -177,13 +184,10 @@ export async function renderRoot(
|
|
|
177
184
|
const childElement = child as VargElement;
|
|
178
185
|
|
|
179
186
|
let path: string | undefined;
|
|
180
|
-
const isVideo =
|
|
181
|
-
childElement.type === "video" || childElement.type === "animate";
|
|
187
|
+
const isVideo = childElement.type === "video";
|
|
182
188
|
|
|
183
189
|
if (childElement.type === "video") {
|
|
184
190
|
path = await renderVideo(childElement as VargElement<"video">, ctx);
|
|
185
|
-
} else if (childElement.type === "animate") {
|
|
186
|
-
path = await renderAnimate(childElement as VargElement<"animate">, ctx);
|
|
187
191
|
} else if (childElement.type === "image") {
|
|
188
192
|
path = await renderImage(childElement as VargElement<"image">, ctx);
|
|
189
193
|
}
|
|
@@ -201,10 +205,42 @@ export async function renderRoot(
|
|
|
201
205
|
}
|
|
202
206
|
}
|
|
203
207
|
|
|
204
|
-
const
|
|
208
|
+
const clipResults = await Promise.allSettled(
|
|
205
209
|
clipElements.map((clipElement) => renderClip(clipElement, ctx)),
|
|
206
210
|
);
|
|
207
211
|
|
|
212
|
+
const failures = clipResults
|
|
213
|
+
.map((r, i) =>
|
|
214
|
+
r.status === "rejected" ? { index: i, reason: r.reason } : null,
|
|
215
|
+
)
|
|
216
|
+
.filter(Boolean) as { index: number; reason: Error }[];
|
|
217
|
+
|
|
218
|
+
if (failures.length > 0) {
|
|
219
|
+
const successCount = clipResults.length - failures.length;
|
|
220
|
+
if (successCount > 0) {
|
|
221
|
+
console.log(
|
|
222
|
+
`\x1b[33mℹ ${successCount} clip(s) cached, ${failures.length} failed\x1b[0m`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const errorCounts = new Map<string, number>();
|
|
226
|
+
for (const f of failures) {
|
|
227
|
+
const msg = f.reason?.message || "Unknown error";
|
|
228
|
+
errorCounts.set(msg, (errorCounts.get(msg) || 0) + 1);
|
|
229
|
+
}
|
|
230
|
+
const errors = [...errorCounts.entries()]
|
|
231
|
+
.map(([msg, count]) => (count > 1 ? `${msg} (x${count})` : msg))
|
|
232
|
+
.join("; ");
|
|
233
|
+
throw new Error(
|
|
234
|
+
`${failures.length} of ${clipResults.length} clips failed: ${errors}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const renderedClips = clipResults.map(
|
|
239
|
+
(r) =>
|
|
240
|
+
(r as PromiseFulfilledResult<Awaited<ReturnType<typeof renderClip>>>)
|
|
241
|
+
.value,
|
|
242
|
+
);
|
|
243
|
+
|
|
208
244
|
const clips: Clip[] = [];
|
|
209
245
|
let currentTime = 0;
|
|
210
246
|
|
|
@@ -255,11 +291,11 @@ export async function renderRoot(
|
|
|
255
291
|
let path: string;
|
|
256
292
|
if (musicProps.src) {
|
|
257
293
|
path = resolvePath(musicProps.src);
|
|
258
|
-
} else if (musicProps.prompt
|
|
294
|
+
} else if (musicProps.prompt) {
|
|
259
295
|
const result = await renderMusic(musicElement, ctx);
|
|
260
296
|
path = result.path;
|
|
261
297
|
} else {
|
|
262
|
-
throw new Error("Music requires either src or prompt
|
|
298
|
+
throw new Error("Music requires either src or prompt");
|
|
263
299
|
}
|
|
264
300
|
|
|
265
301
|
audioTracks.push({
|
|
@@ -267,6 +303,7 @@ export async function renderRoot(
|
|
|
267
303
|
mixVolume: musicProps.volume ?? 1,
|
|
268
304
|
cutFrom,
|
|
269
305
|
cutTo,
|
|
306
|
+
start: musicProps.start,
|
|
270
307
|
});
|
|
271
308
|
}
|
|
272
309
|
|
|
@@ -280,40 +317,59 @@ export async function renderRoot(
|
|
|
280
317
|
const editlyTaskId = addTask(progress, "editly", "ffmpeg");
|
|
281
318
|
startTask(progress, editlyTaskId);
|
|
282
319
|
|
|
283
|
-
await editly({
|
|
320
|
+
const editlyResult = await editly({
|
|
284
321
|
outPath: tempOutPath,
|
|
285
322
|
width: ctx.width,
|
|
286
323
|
height: ctx.height,
|
|
287
324
|
fps: ctx.fps,
|
|
288
325
|
clips,
|
|
289
326
|
audioTracks: audioTracks.length > 0 ? audioTracks : undefined,
|
|
327
|
+
shortest: props.shortest,
|
|
328
|
+
verbose: options.verbose,
|
|
329
|
+
backend: options.backend,
|
|
290
330
|
});
|
|
291
331
|
|
|
292
332
|
completeTask(progress, editlyTaskId);
|
|
293
333
|
|
|
334
|
+
let output = editlyResult.output;
|
|
335
|
+
|
|
294
336
|
if (hasCaptions && captionsResult) {
|
|
295
337
|
const captionsTaskId = addTask(progress, "captions", "ffmpeg");
|
|
296
338
|
startTask(progress, captionsTaskId);
|
|
297
339
|
|
|
298
|
-
|
|
299
|
-
|
|
340
|
+
output = await burnCaptions({
|
|
341
|
+
video: output,
|
|
342
|
+
assPath: captionsResult.assPath,
|
|
343
|
+
outputPath: finalOutPath,
|
|
344
|
+
backend: options.backend,
|
|
345
|
+
verbose: options.verbose,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (!options.backend) {
|
|
349
|
+
ctx.tempFiles.push(tempOutPath);
|
|
350
|
+
}
|
|
300
351
|
|
|
301
|
-
ctx.tempFiles.push(tempOutPath);
|
|
302
352
|
completeTask(progress, captionsTaskId);
|
|
303
353
|
}
|
|
304
354
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
);
|
|
355
|
+
let finalBuffer: ArrayBuffer;
|
|
356
|
+
if (output.type === "url") {
|
|
357
|
+
const res = await fetch(output.url);
|
|
358
|
+
if (!res.ok)
|
|
359
|
+
throw new Error(`Failed to download final render: ${res.status}`);
|
|
360
|
+
finalBuffer = await res.arrayBuffer();
|
|
361
|
+
if (options.output) {
|
|
362
|
+
await Bun.write(options.output, finalBuffer);
|
|
314
363
|
}
|
|
364
|
+
} else {
|
|
365
|
+
finalBuffer = await Bun.file(output.path).arrayBuffer();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!options.quiet && mode === "preview" && placeholderCount.total > 0) {
|
|
369
|
+
console.log(
|
|
370
|
+
`\x1b[36mℹ preview mode: ${placeholderCount.total} placeholders used (${placeholderCount.images} images, ${placeholderCount.videos} videos)\x1b[0m`,
|
|
371
|
+
);
|
|
315
372
|
}
|
|
316
373
|
|
|
317
|
-
|
|
318
|
-
return new Uint8Array(result);
|
|
374
|
+
return new Uint8Array(finalBuffer);
|
|
319
375
|
}
|
|
@@ -21,9 +21,9 @@ export async function renderSpeech(
|
|
|
21
21
|
throw new Error("Speech element requires text content");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const model = props.model;
|
|
24
|
+
const model = props.model ?? ctx.defaults?.speech;
|
|
25
25
|
if (!model) {
|
|
26
|
-
throw new Error("Speech
|
|
26
|
+
throw new Error("Speech requires 'model' prop (or set defaults.speech)");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const cacheKey = computeCacheKey(element);
|