varg.ai-sdk 0.1.1 → 0.4.0-alpha.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.
Files changed (246) hide show
  1. package/.claude/settings.local.json +1 -1
  2. package/.env.example +3 -0
  3. package/.github/workflows/ci.yml +23 -0
  4. package/.husky/README.md +102 -0
  5. package/.husky/commit-msg +6 -0
  6. package/.husky/pre-commit +9 -0
  7. package/.husky/pre-push +6 -0
  8. package/.size-limit.json +8 -0
  9. package/.test-hooks.ts +5 -0
  10. package/CLAUDE.md +10 -3
  11. package/CONTRIBUTING.md +150 -0
  12. package/LICENSE.md +53 -0
  13. package/README.md +56 -209
  14. package/SKILLS.md +26 -10
  15. package/biome.json +7 -1
  16. package/bun.lock +1286 -0
  17. package/commitlint.config.js +22 -0
  18. package/docs/index.html +1130 -0
  19. package/docs/prompting.md +326 -0
  20. package/docs/react.md +834 -0
  21. package/docs/sdk.md +812 -0
  22. package/ffmpeg/CLAUDE.md +68 -0
  23. package/package.json +43 -10
  24. package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
  25. package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
  26. package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
  27. package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
  28. package/pipeline/cookbooks/text-to-tiktok.md +669 -0
  29. package/pipeline/cookbooks/trendwatching.md +156 -0
  30. package/plan.md +281 -0
  31. package/scripts/.gitkeep +0 -0
  32. package/src/ai-sdk/cache.ts +142 -0
  33. package/src/ai-sdk/examples/cached-generation.ts +53 -0
  34. package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
  35. package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
  36. package/src/ai-sdk/examples/duet-video.ts +56 -0
  37. package/src/ai-sdk/examples/editly-composition.ts +63 -0
  38. package/src/ai-sdk/examples/editly-test.ts +57 -0
  39. package/src/ai-sdk/examples/editly-video-test.ts +52 -0
  40. package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
  41. package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
  42. package/src/ai-sdk/examples/music-generation.ts +19 -0
  43. package/src/ai-sdk/examples/openai-sora.ts +34 -0
  44. package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
  45. package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
  46. package/src/ai-sdk/examples/talking-lion.ts +55 -0
  47. package/src/ai-sdk/examples/video-generation.ts +39 -0
  48. package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
  49. package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
  50. package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
  51. package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
  52. package/src/ai-sdk/file-cache.ts +112 -0
  53. package/src/ai-sdk/file.ts +238 -0
  54. package/src/ai-sdk/generate-element.ts +92 -0
  55. package/src/ai-sdk/generate-music.ts +46 -0
  56. package/src/ai-sdk/generate-video.ts +165 -0
  57. package/src/ai-sdk/index.ts +72 -0
  58. package/src/ai-sdk/music-model.ts +110 -0
  59. package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
  60. package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
  61. package/src/ai-sdk/providers/editly/index.ts +817 -0
  62. package/src/ai-sdk/providers/editly/layers.ts +776 -0
  63. package/src/ai-sdk/providers/editly/plan.md +144 -0
  64. package/src/ai-sdk/providers/editly/types.ts +328 -0
  65. package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
  66. package/src/ai-sdk/providers/fal-provider.ts +512 -0
  67. package/src/ai-sdk/providers/higgsfield.ts +379 -0
  68. package/src/ai-sdk/providers/openai.ts +251 -0
  69. package/src/ai-sdk/providers/replicate.ts +16 -0
  70. package/src/ai-sdk/video-model.ts +185 -0
  71. package/src/cli/commands/find.tsx +137 -0
  72. package/src/cli/commands/help.tsx +85 -0
  73. package/src/cli/commands/index.ts +6 -0
  74. package/src/cli/commands/list.tsx +238 -0
  75. package/src/cli/commands/render.tsx +71 -0
  76. package/src/cli/commands/run.tsx +511 -0
  77. package/src/cli/commands/which.tsx +253 -0
  78. package/src/cli/index.ts +114 -0
  79. package/src/cli/quiet.ts +44 -0
  80. package/src/cli/types.ts +32 -0
  81. package/src/cli/ui/components/Badge.tsx +29 -0
  82. package/src/cli/ui/components/DataTable.tsx +51 -0
  83. package/src/cli/ui/components/Header.tsx +23 -0
  84. package/src/cli/ui/components/HelpBlock.tsx +44 -0
  85. package/src/cli/ui/components/KeyValue.tsx +33 -0
  86. package/src/cli/ui/components/OptionRow.tsx +81 -0
  87. package/src/cli/ui/components/Separator.tsx +23 -0
  88. package/src/cli/ui/components/StatusBox.tsx +108 -0
  89. package/src/cli/ui/components/VargBox.tsx +51 -0
  90. package/src/cli/ui/components/VargProgress.tsx +36 -0
  91. package/src/cli/ui/components/VargSpinner.tsx +34 -0
  92. package/src/cli/ui/components/VargText.tsx +56 -0
  93. package/src/cli/ui/components/index.ts +19 -0
  94. package/src/cli/ui/index.ts +12 -0
  95. package/src/cli/ui/render.ts +35 -0
  96. package/src/cli/ui/theme.ts +63 -0
  97. package/src/cli/utils.ts +78 -0
  98. package/src/core/executor/executor.ts +201 -0
  99. package/src/core/executor/index.ts +13 -0
  100. package/src/core/executor/job.ts +214 -0
  101. package/src/core/executor/pipeline.ts +222 -0
  102. package/src/core/index.ts +11 -0
  103. package/src/core/registry/index.ts +9 -0
  104. package/src/core/registry/loader.ts +149 -0
  105. package/src/core/registry/registry.ts +221 -0
  106. package/src/core/registry/resolver.ts +206 -0
  107. package/src/core/schema/helpers.ts +134 -0
  108. package/src/core/schema/index.ts +8 -0
  109. package/src/core/schema/shared.ts +102 -0
  110. package/src/core/schema/types.ts +279 -0
  111. package/src/core/schema/validator.ts +92 -0
  112. package/src/definitions/actions/captions.ts +261 -0
  113. package/src/definitions/actions/edit.ts +298 -0
  114. package/src/definitions/actions/image.ts +125 -0
  115. package/src/definitions/actions/index.ts +114 -0
  116. package/src/definitions/actions/music.ts +205 -0
  117. package/src/definitions/actions/sync.ts +128 -0
  118. package/{action/transcribe/index.ts → src/definitions/actions/transcribe.ts} +58 -68
  119. package/src/definitions/actions/upload.ts +111 -0
  120. package/src/definitions/actions/video.ts +163 -0
  121. package/src/definitions/actions/voice.ts +119 -0
  122. package/src/definitions/index.ts +23 -0
  123. package/src/definitions/models/elevenlabs.ts +50 -0
  124. package/src/definitions/models/flux.ts +56 -0
  125. package/src/definitions/models/index.ts +36 -0
  126. package/src/definitions/models/kling.ts +56 -0
  127. package/src/definitions/models/llama.ts +54 -0
  128. package/src/definitions/models/nano-banana-pro.ts +102 -0
  129. package/src/definitions/models/sonauto.ts +68 -0
  130. package/src/definitions/models/soul.ts +65 -0
  131. package/src/definitions/models/wan.ts +54 -0
  132. package/src/definitions/models/whisper.ts +44 -0
  133. package/src/definitions/skills/index.ts +12 -0
  134. package/src/definitions/skills/talking-character.ts +87 -0
  135. package/src/definitions/skills/text-to-tiktok.ts +97 -0
  136. package/src/index.ts +118 -0
  137. package/src/providers/apify.ts +269 -0
  138. package/src/providers/base.ts +264 -0
  139. package/src/providers/elevenlabs.ts +217 -0
  140. package/src/providers/fal.ts +392 -0
  141. package/src/providers/ffmpeg.ts +544 -0
  142. package/src/providers/fireworks.ts +193 -0
  143. package/src/providers/groq.ts +149 -0
  144. package/src/providers/higgsfield.ts +145 -0
  145. package/src/providers/index.ts +143 -0
  146. package/src/providers/replicate.ts +147 -0
  147. package/src/providers/storage.ts +206 -0
  148. package/src/react/cli.ts +52 -0
  149. package/src/react/elements.ts +146 -0
  150. package/src/react/examples/branching.tsx +66 -0
  151. package/src/react/examples/captions-demo.tsx +37 -0
  152. package/src/react/examples/character-video.tsx +84 -0
  153. package/src/react/examples/grid.tsx +53 -0
  154. package/src/react/examples/layouts-demo.tsx +57 -0
  155. package/src/react/examples/madi.tsx +60 -0
  156. package/src/react/examples/music-test.tsx +35 -0
  157. package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
  158. package/src/react/examples/orange-portrait.tsx +41 -0
  159. package/src/react/examples/split-element-demo.tsx +60 -0
  160. package/src/react/examples/split-layout-demo.tsx +60 -0
  161. package/src/react/examples/split.tsx +41 -0
  162. package/src/react/examples/video-grid.tsx +46 -0
  163. package/src/react/index.ts +43 -0
  164. package/src/react/layouts/grid.tsx +28 -0
  165. package/src/react/layouts/index.ts +2 -0
  166. package/src/react/layouts/split.tsx +20 -0
  167. package/src/react/react.test.ts +309 -0
  168. package/src/react/render.ts +21 -0
  169. package/src/react/renderers/animate.ts +59 -0
  170. package/src/react/renderers/captions.ts +297 -0
  171. package/src/react/renderers/clip.ts +248 -0
  172. package/src/react/renderers/context.ts +17 -0
  173. package/src/react/renderers/image.ts +109 -0
  174. package/src/react/renderers/index.ts +22 -0
  175. package/src/react/renderers/music.ts +60 -0
  176. package/src/react/renderers/packshot.ts +84 -0
  177. package/src/react/renderers/progress.ts +173 -0
  178. package/src/react/renderers/render.ts +243 -0
  179. package/src/react/renderers/slider.ts +69 -0
  180. package/src/react/renderers/speech.ts +53 -0
  181. package/src/react/renderers/split.ts +91 -0
  182. package/src/react/renderers/subtitle.ts +16 -0
  183. package/src/react/renderers/swipe.ts +75 -0
  184. package/src/react/renderers/title.ts +17 -0
  185. package/src/react/renderers/utils.ts +124 -0
  186. package/src/react/renderers/video.ts +127 -0
  187. package/src/react/runtime/jsx-dev-runtime.ts +43 -0
  188. package/src/react/runtime/jsx-runtime.ts +35 -0
  189. package/src/react/types.ts +232 -0
  190. package/src/studio/index.ts +26 -0
  191. package/src/studio/scanner.ts +102 -0
  192. package/src/studio/server.ts +554 -0
  193. package/src/studio/stages.ts +251 -0
  194. package/src/studio/step-renderer.ts +279 -0
  195. package/src/studio/types.ts +60 -0
  196. package/src/studio/ui/cache.html +303 -0
  197. package/src/studio/ui/index.html +1820 -0
  198. package/src/tests/all.test.ts +509 -0
  199. package/src/tests/index.ts +33 -0
  200. package/src/tests/unit.test.ts +403 -0
  201. package/tsconfig.cli.json +8 -0
  202. package/tsconfig.json +21 -3
  203. package/TEST_RESULTS.md +0 -122
  204. package/action/captions/SKILL.md +0 -170
  205. package/action/captions/index.ts +0 -169
  206. package/action/edit/SKILL.md +0 -235
  207. package/action/edit/index.ts +0 -437
  208. package/action/image/SKILL.md +0 -140
  209. package/action/image/index.ts +0 -105
  210. package/action/sync/SKILL.md +0 -136
  211. package/action/sync/index.ts +0 -145
  212. package/action/transcribe/SKILL.md +0 -179
  213. package/action/video/SKILL.md +0 -116
  214. package/action/video/index.ts +0 -125
  215. package/action/voice/SKILL.md +0 -125
  216. package/action/voice/index.ts +0 -136
  217. package/cli/commands/find.ts +0 -58
  218. package/cli/commands/help.ts +0 -70
  219. package/cli/commands/list.ts +0 -49
  220. package/cli/commands/run.ts +0 -237
  221. package/cli/commands/which.ts +0 -66
  222. package/cli/discover.ts +0 -66
  223. package/cli/index.ts +0 -33
  224. package/cli/runner.ts +0 -65
  225. package/cli/types.ts +0 -49
  226. package/cli/ui.ts +0 -185
  227. package/index.ts +0 -75
  228. package/lib/README.md +0 -144
  229. package/lib/ai-sdk/fal.ts +0 -106
  230. package/lib/ai-sdk/replicate.ts +0 -107
  231. package/lib/elevenlabs.ts +0 -382
  232. package/lib/fal.ts +0 -467
  233. package/lib/ffmpeg.ts +0 -467
  234. package/lib/fireworks.ts +0 -235
  235. package/lib/groq.ts +0 -246
  236. package/lib/higgsfield.ts +0 -176
  237. package/lib/remotion/SKILL.md +0 -823
  238. package/lib/remotion/cli.ts +0 -115
  239. package/lib/remotion/functions.ts +0 -283
  240. package/lib/remotion/index.ts +0 -19
  241. package/lib/remotion/templates.ts +0 -73
  242. package/lib/replicate.ts +0 -304
  243. package/output.txt +0 -1
  244. package/test-import.ts +0 -7
  245. package/test-services.ts +0 -97
  246. package/utilities/s3.ts +0 -147
@@ -0,0 +1,60 @@
1
+ import { generateMusic } from "../../ai-sdk/generate-music";
2
+ import type { MusicProps, VargElement } from "../types";
3
+ import type { RenderContext } from "./context";
4
+ import { addTask, completeTask, startTask } from "./progress";
5
+
6
+ export async function renderMusic(
7
+ element: VargElement<"music">,
8
+ ctx: RenderContext,
9
+ ): Promise<{ path: string }> {
10
+ const props = element.props as MusicProps;
11
+
12
+ const prompt = props.prompt;
13
+ const model = props.model;
14
+ if (!prompt || !model) {
15
+ throw new Error("Music generation requires both prompt and model");
16
+ }
17
+
18
+ const cacheKey = JSON.stringify({
19
+ type: "music",
20
+ prompt,
21
+ model: model.modelId,
22
+ duration: props.duration,
23
+ });
24
+
25
+ const modelId = model.modelId;
26
+ const taskId = ctx.progress ? addTask(ctx.progress, "music", modelId) : null;
27
+
28
+ const generateFn = async () => {
29
+ const result = await generateMusic({
30
+ model,
31
+ prompt,
32
+ duration: props.duration,
33
+ });
34
+ return result.audio.uint8Array;
35
+ };
36
+
37
+ let audioData: Uint8Array;
38
+
39
+ if (ctx.cache) {
40
+ const cached = await ctx.cache.get(cacheKey);
41
+ if (cached) {
42
+ audioData = cached as Uint8Array;
43
+ } else {
44
+ if (taskId && ctx.progress) startTask(ctx.progress, taskId);
45
+ audioData = await generateFn();
46
+ if (taskId && ctx.progress) completeTask(ctx.progress, taskId);
47
+ await ctx.cache.set(cacheKey, audioData);
48
+ }
49
+ } else {
50
+ if (taskId && ctx.progress) startTask(ctx.progress, taskId);
51
+ audioData = await generateFn();
52
+ if (taskId && ctx.progress) completeTask(ctx.progress, taskId);
53
+ }
54
+
55
+ const tempPath = `/tmp/varg-music-${Date.now()}.mp3`;
56
+ await Bun.write(tempPath, audioData);
57
+ ctx.tempFiles.push(tempPath);
58
+
59
+ return { path: tempPath };
60
+ }
@@ -0,0 +1,84 @@
1
+ import { editly } from "../../ai-sdk/providers/editly";
2
+ import type {
3
+ Clip,
4
+ ImageOverlayLayer,
5
+ Layer,
6
+ Position,
7
+ TitleLayer,
8
+ } from "../../ai-sdk/providers/editly/types";
9
+ import type { PackshotProps, VargElement } from "../types";
10
+ import type { RenderContext } from "./context";
11
+ import { renderImage } from "./image";
12
+
13
+ function resolvePosition(pos: Position | undefined): Position {
14
+ return pos ?? "center";
15
+ }
16
+
17
+ export async function renderPackshot(
18
+ element: VargElement<"packshot">,
19
+ ctx: RenderContext,
20
+ ): Promise<string> {
21
+ const props = element.props as PackshotProps;
22
+ const duration = props.duration ?? 3;
23
+
24
+ const layers: Layer[] = [];
25
+
26
+ if (props.background) {
27
+ if (typeof props.background === "string") {
28
+ layers.push({
29
+ type: "fill-color" as const,
30
+ color: props.background,
31
+ });
32
+ } else {
33
+ const bgPath = await renderImage(props.background, ctx);
34
+ layers.push({
35
+ type: "image" as const,
36
+ path: bgPath,
37
+ resizeMode: "cover" as const,
38
+ });
39
+ }
40
+ } else {
41
+ layers.push({
42
+ type: "fill-color" as const,
43
+ color: "#000000",
44
+ });
45
+ }
46
+
47
+ if (props.logo) {
48
+ const logoLayer: ImageOverlayLayer = {
49
+ type: "image-overlay",
50
+ path: props.logo,
51
+ position: resolvePosition(props.logoPosition),
52
+ width: props.logoSize ?? "30%",
53
+ };
54
+ layers.push(logoLayer);
55
+ }
56
+
57
+ if (props.cta) {
58
+ const ctaLayer: TitleLayer = {
59
+ type: "title",
60
+ text: props.cta,
61
+ textColor: props.ctaColor ?? "white",
62
+ position: resolvePosition(props.ctaPosition ?? "bottom"),
63
+ };
64
+ layers.push(ctaLayer);
65
+ }
66
+
67
+ const clip: Clip = {
68
+ layers,
69
+ duration,
70
+ };
71
+
72
+ const outPath = `/tmp/varg-packshot-${Date.now()}.mp4`;
73
+
74
+ await editly({
75
+ outPath,
76
+ width: ctx.width,
77
+ height: ctx.height,
78
+ fps: ctx.fps,
79
+ clips: [clip],
80
+ });
81
+
82
+ ctx.tempFiles.push(outPath);
83
+ return outPath;
84
+ }
@@ -0,0 +1,173 @@
1
+ export type GenerationType =
2
+ | "image"
3
+ | "video"
4
+ | "animate"
5
+ | "speech"
6
+ | "music"
7
+ | "editly"
8
+ | "captions"
9
+ | "transcribe";
10
+
11
+ export const TIME_ESTIMATES: Record<GenerationType, number> = {
12
+ image: 30,
13
+ video: 120,
14
+ animate: 90,
15
+ speech: 5,
16
+ music: 45,
17
+ editly: 15,
18
+ captions: 10,
19
+ transcribe: 15,
20
+ };
21
+
22
+ export const MODEL_TIME_ESTIMATES: Record<string, number> = {
23
+ // image models - use partial matching
24
+ "flux/schnell": 3,
25
+ "flux/dev": 8,
26
+ "flux-pro": 12,
27
+ recraft: 10,
28
+ ideogram: 8,
29
+ // video models
30
+ kling: 180,
31
+ "kling-v2": 180,
32
+ "kling-v2.5": 180,
33
+ minimax: 90,
34
+ luma: 90,
35
+ runway: 45,
36
+ veo: 120,
37
+ // speech models
38
+ elevenlabs: 5,
39
+ eleven: 5,
40
+ // music models
41
+ "stable-audio": 30,
42
+ musicgen: 45,
43
+ };
44
+
45
+ export interface ProgressTask {
46
+ id: string;
47
+ type: GenerationType;
48
+ model: string;
49
+ status: "pending" | "running" | "done";
50
+ startedAt?: number;
51
+ completedAt?: number;
52
+ }
53
+
54
+ export interface ProgressTracker {
55
+ tasks: ProgressTask[];
56
+ onUpdate?: (tracker: ProgressTracker) => void;
57
+ quiet: boolean;
58
+ }
59
+
60
+ export function createProgressTracker(quiet = false): ProgressTracker {
61
+ return {
62
+ tasks: [],
63
+ quiet,
64
+ };
65
+ }
66
+
67
+ export function addTask(
68
+ tracker: ProgressTracker,
69
+ type: GenerationType,
70
+ model: string,
71
+ ): string {
72
+ const id = `${type}-${tracker.tasks.length}`;
73
+ tracker.tasks.push({ id, type, model, status: "pending" });
74
+ return id;
75
+ }
76
+
77
+ export function startTask(tracker: ProgressTracker, id: string): void {
78
+ const task = tracker.tasks.find((t) => t.id === id);
79
+ if (task) {
80
+ task.status = "running";
81
+ task.startedAt = Date.now();
82
+ if (!tracker.quiet) {
83
+ logTaskStart(task);
84
+ }
85
+ }
86
+ tracker.onUpdate?.(tracker);
87
+ }
88
+
89
+ const CACHE_THRESHOLD_MS = 1000;
90
+
91
+ export function completeTask(tracker: ProgressTracker, id: string): void {
92
+ const task = tracker.tasks.find((t) => t.id === id);
93
+ if (task) {
94
+ task.status = "done";
95
+ task.completedAt = Date.now();
96
+ const duration = task.startedAt ? task.completedAt - task.startedAt : 0;
97
+ const wasCached = duration < CACHE_THRESHOLD_MS;
98
+ if (!tracker.quiet) {
99
+ logTaskComplete(task, wasCached);
100
+ }
101
+ }
102
+ tracker.onUpdate?.(tracker);
103
+ }
104
+
105
+ function getEstimate(task: ProgressTask): number {
106
+ const modelLower = task.model.toLowerCase();
107
+ for (const [key, estimate] of Object.entries(MODEL_TIME_ESTIMATES)) {
108
+ if (modelLower.includes(key.toLowerCase())) {
109
+ return estimate;
110
+ }
111
+ }
112
+ return TIME_ESTIMATES[task.type];
113
+ }
114
+
115
+ function logTaskStart(task: ProgressTask): void {
116
+ const estimate = getEstimate(task);
117
+ console.log(`⏳ generating ${task.type} with ${task.model} (~${estimate}s)`);
118
+ }
119
+
120
+ function logTaskComplete(task: ProgressTask, cached: boolean): void {
121
+ const duration =
122
+ task.completedAt && task.startedAt
123
+ ? ((task.completedAt - task.startedAt) / 1000).toFixed(1)
124
+ : "?";
125
+ if (cached) {
126
+ console.log(`⚡ ${task.type} from cache`);
127
+ } else {
128
+ console.log(`✓ ${task.type} done (${duration}s)`);
129
+ }
130
+ }
131
+
132
+ export function getTotalEstimate(tracker: ProgressTracker): number {
133
+ return tracker.tasks.reduce((sum, task) => sum + getEstimate(task), 0);
134
+ }
135
+
136
+ export function getElapsedTime(tracker: ProgressTracker): number {
137
+ const completed = tracker.tasks.filter((t) => t.status === "done");
138
+ const running = tracker.tasks.find((t) => t.status === "running");
139
+
140
+ let elapsed = 0;
141
+ for (const task of completed) {
142
+ if (task.startedAt && task.completedAt) {
143
+ elapsed += (task.completedAt - task.startedAt) / 1000;
144
+ }
145
+ }
146
+ if (running?.startedAt) {
147
+ elapsed += (Date.now() - running.startedAt) / 1000;
148
+ }
149
+ return elapsed;
150
+ }
151
+
152
+ export function getProgress(tracker: ProgressTracker): number {
153
+ const total = tracker.tasks.length;
154
+ if (total === 0) return 0;
155
+ const done = tracker.tasks.filter((t) => t.status === "done").length;
156
+ return Math.round((done / total) * 100);
157
+ }
158
+
159
+ export function renderProgressBar(tracker: ProgressTracker): string {
160
+ const progress = getProgress(tracker);
161
+ const total = getTotalEstimate(tracker);
162
+ const elapsed = getElapsedTime(tracker);
163
+
164
+ const width = 30;
165
+ const filled = Math.round((progress / 100) * width);
166
+ const empty = width - filled;
167
+ const bar = "█".repeat(filled) + "░".repeat(empty);
168
+
169
+ const done = tracker.tasks.filter((t) => t.status === "done").length;
170
+ const totalTasks = tracker.tasks.length;
171
+
172
+ return `[${bar}] ${progress}% (${done}/${totalTasks} tasks, ~${Math.round(total - elapsed)}s remaining)`;
173
+ }
@@ -0,0 +1,243 @@
1
+ import { generateImage } from "ai";
2
+ import { withCache } from "../../ai-sdk/cache";
3
+ import { fileCache } from "../../ai-sdk/file-cache";
4
+ import { generateVideo } from "../../ai-sdk/generate-video";
5
+ import { editly } from "../../ai-sdk/providers/editly";
6
+ import type {
7
+ AudioTrack,
8
+ Clip,
9
+ Layer,
10
+ VideoLayer,
11
+ } from "../../ai-sdk/providers/editly/types";
12
+ import type {
13
+ CaptionsProps,
14
+ ClipProps,
15
+ MusicProps,
16
+ OverlayProps,
17
+ RenderOptions,
18
+ RenderProps,
19
+ SpeechProps,
20
+ VargElement,
21
+ } from "../types";
22
+ import { renderAnimate } from "./animate";
23
+ import { renderCaptions } from "./captions";
24
+ import { renderClip } from "./clip";
25
+ import type { RenderContext } from "./context";
26
+ import { renderImage } from "./image";
27
+ import { renderMusic } from "./music";
28
+ import {
29
+ addTask,
30
+ completeTask,
31
+ createProgressTracker,
32
+ startTask,
33
+ } from "./progress";
34
+ import { renderSpeech } from "./speech";
35
+ import { resolvePath } from "./utils";
36
+ import { renderVideo } from "./video";
37
+
38
+ interface RenderedOverlay {
39
+ path: string;
40
+ props: OverlayProps;
41
+ isVideo: boolean;
42
+ }
43
+
44
+ export async function renderRoot(
45
+ element: VargElement<"render">,
46
+ options: RenderOptions,
47
+ ): Promise<Uint8Array> {
48
+ const props = element.props as RenderProps;
49
+ const progress = createProgressTracker(options.quiet ?? false);
50
+
51
+ const ctx: RenderContext = {
52
+ width: props.width ?? 1920,
53
+ height: props.height ?? 1080,
54
+ fps: props.fps ?? 30,
55
+ cache: options.cache ? fileCache({ dir: options.cache }) : undefined,
56
+ generateImage: options.cache
57
+ ? withCache(generateImage, { storage: fileCache({ dir: options.cache }) })
58
+ : generateImage,
59
+ generateVideo: options.cache
60
+ ? withCache(generateVideo, { storage: fileCache({ dir: options.cache }) })
61
+ : generateVideo,
62
+ tempFiles: [],
63
+ progress,
64
+ pending: new Map(),
65
+ };
66
+
67
+ const clipElements: VargElement<"clip">[] = [];
68
+ const overlayElements: VargElement<"overlay">[] = [];
69
+ const musicElements: VargElement<"music">[] = [];
70
+ const audioTracks: AudioTrack[] = [];
71
+ let captionsResult: Awaited<ReturnType<typeof renderCaptions>> | undefined;
72
+
73
+ for (const child of element.children) {
74
+ if (!child || typeof child !== "object" || !("type" in child)) continue;
75
+
76
+ const childElement = child as VargElement;
77
+
78
+ if (childElement.type === "clip") {
79
+ clipElements.push(childElement as VargElement<"clip">);
80
+ } else if (childElement.type === "overlay") {
81
+ overlayElements.push(childElement as VargElement<"overlay">);
82
+ } else if (childElement.type === "captions") {
83
+ captionsResult = await renderCaptions(
84
+ childElement as VargElement<"captions">,
85
+ ctx,
86
+ );
87
+ if (captionsResult.audioPath) {
88
+ audioTracks.push({
89
+ path: captionsResult.audioPath,
90
+ mixVolume: 1,
91
+ });
92
+ }
93
+ } else if (childElement.type === "speech") {
94
+ const result = await renderSpeech(
95
+ childElement as VargElement<"speech">,
96
+ ctx,
97
+ );
98
+ const speechProps = childElement.props as SpeechProps;
99
+ audioTracks.push({
100
+ path: result.path,
101
+ mixVolume: speechProps.volume ?? 1,
102
+ });
103
+ } else if (childElement.type === "music") {
104
+ musicElements.push(childElement as VargElement<"music">);
105
+ }
106
+ }
107
+
108
+ const renderedOverlays: RenderedOverlay[] = [];
109
+ for (const overlay of overlayElements) {
110
+ const overlayProps = overlay.props as OverlayProps;
111
+ for (const child of overlay.children) {
112
+ if (!child || typeof child !== "object" || !("type" in child)) continue;
113
+ const childElement = child as VargElement;
114
+
115
+ let path: string | undefined;
116
+ const isVideo =
117
+ childElement.type === "video" || childElement.type === "animate";
118
+
119
+ if (childElement.type === "video") {
120
+ path = await renderVideo(childElement as VargElement<"video">, ctx);
121
+ } else if (childElement.type === "animate") {
122
+ path = await renderAnimate(childElement as VargElement<"animate">, ctx);
123
+ } else if (childElement.type === "image") {
124
+ path = await renderImage(childElement as VargElement<"image">, ctx);
125
+ }
126
+
127
+ if (path) {
128
+ renderedOverlays.push({ path, props: overlayProps, isVideo });
129
+
130
+ if (isVideo && overlayProps.keepAudio) {
131
+ audioTracks.push({
132
+ path,
133
+ mixVolume: overlayProps.volume ?? 1,
134
+ });
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ const renderedClips = await Promise.all(
141
+ clipElements.map((clipElement) => renderClip(clipElement, ctx)),
142
+ );
143
+
144
+ const clips: Clip[] = [];
145
+ let currentTime = 0;
146
+
147
+ for (let i = 0; i < clipElements.length; i++) {
148
+ const clipElement = clipElements[i];
149
+ const clip = renderedClips[i];
150
+ if (!clipElement || !clip) {
151
+ throw new Error(`Missing clip data at index ${i}`);
152
+ }
153
+ const clipProps = clipElement.props as ClipProps;
154
+ const clipDuration =
155
+ typeof clipProps.duration === "number" ? clipProps.duration : 3;
156
+
157
+ for (const overlay of renderedOverlays) {
158
+ const overlayLayer: VideoLayer = {
159
+ type: "video",
160
+ path: overlay.path,
161
+ cutFrom: currentTime,
162
+ cutTo: currentTime + clipDuration,
163
+ left: overlay.props.left,
164
+ top: overlay.props.top,
165
+ width: overlay.props.width,
166
+ height: overlay.props.height,
167
+ };
168
+ clip.layers.push(overlayLayer as Layer);
169
+ }
170
+
171
+ clips.push(clip);
172
+
173
+ currentTime += clipDuration;
174
+ if (i < clipElements.length - 1 && clip.transition) {
175
+ currentTime -= clip.transition.duration ?? 0;
176
+ }
177
+ }
178
+
179
+ const totalDuration = currentTime;
180
+
181
+ // process music after clips so we know total duration for auto-trim
182
+ for (const musicElement of musicElements) {
183
+ const musicProps = musicElement.props as MusicProps;
184
+ const cutFrom = musicProps.cutFrom ?? 0;
185
+ const cutTo =
186
+ musicProps.cutTo ??
187
+ (musicProps.duration !== undefined
188
+ ? cutFrom + musicProps.duration
189
+ : totalDuration); // auto-trim to video length
190
+
191
+ let path: string;
192
+ if (musicProps.src) {
193
+ path = resolvePath(musicProps.src);
194
+ } else if (musicProps.prompt && musicProps.model) {
195
+ const result = await renderMusic(musicElement, ctx);
196
+ path = result.path;
197
+ } else {
198
+ throw new Error("Music requires either src or prompt+model");
199
+ }
200
+
201
+ audioTracks.push({
202
+ path,
203
+ mixVolume: musicProps.volume ?? 1,
204
+ cutFrom,
205
+ cutTo,
206
+ });
207
+ }
208
+
209
+ const hasCaptions = captionsResult !== undefined;
210
+
211
+ const tempOutPath = hasCaptions
212
+ ? `/tmp/varg-pre-captions-${Date.now()}.mp4`
213
+ : (options.output ?? `output/varg-${Date.now()}.mp4`);
214
+ const finalOutPath = options.output ?? `output/varg-${Date.now()}.mp4`;
215
+
216
+ const editlyTaskId = addTask(progress, "editly", "ffmpeg");
217
+ startTask(progress, editlyTaskId);
218
+
219
+ await editly({
220
+ outPath: tempOutPath,
221
+ width: ctx.width,
222
+ height: ctx.height,
223
+ fps: ctx.fps,
224
+ clips,
225
+ audioTracks: audioTracks.length > 0 ? audioTracks : undefined,
226
+ });
227
+
228
+ completeTask(progress, editlyTaskId);
229
+
230
+ if (hasCaptions && captionsResult) {
231
+ const captionsTaskId = addTask(progress, "captions", "ffmpeg");
232
+ startTask(progress, captionsTaskId);
233
+
234
+ const { $ } = await import("bun");
235
+ await $`ffmpeg -y -i ${tempOutPath} -vf "ass=${captionsResult.assPath}" -c:a copy ${finalOutPath}`.quiet();
236
+
237
+ ctx.tempFiles.push(tempOutPath);
238
+ completeTask(progress, captionsTaskId);
239
+ }
240
+
241
+ const result = await Bun.file(finalOutPath).arrayBuffer();
242
+ return new Uint8Array(result);
243
+ }
@@ -0,0 +1,69 @@
1
+ import { editly } from "../../ai-sdk/providers/editly";
2
+ import type { Clip } from "../../ai-sdk/providers/editly/types";
3
+ import type { SliderProps, VargElement } from "../types";
4
+ import type { RenderContext } from "./context";
5
+ import { renderImage } from "./image";
6
+ import { renderVideo } from "./video";
7
+
8
+ export async function renderSlider(
9
+ element: VargElement<"slider">,
10
+ ctx: RenderContext,
11
+ ): Promise<string> {
12
+ const props = element.props as SliderProps;
13
+ const direction = props.direction ?? "horizontal";
14
+
15
+ const childPaths: string[] = [];
16
+
17
+ for (const child of element.children) {
18
+ if (!child || typeof child !== "object" || !("type" in child)) continue;
19
+ const childElement = child as VargElement;
20
+
21
+ if (childElement.type === "image") {
22
+ const path = await renderImage(childElement as VargElement<"image">, ctx);
23
+ childPaths.push(path);
24
+ } else if (childElement.type === "video") {
25
+ const path = await renderVideo(childElement as VargElement<"video">, ctx);
26
+ childPaths.push(path);
27
+ }
28
+ }
29
+
30
+ if (childPaths.length === 0) {
31
+ throw new Error(
32
+ "Slider element requires at least one image or video child",
33
+ );
34
+ }
35
+
36
+ if (childPaths.length === 1) {
37
+ return childPaths[0]!;
38
+ }
39
+
40
+ const transitionName = direction === "horizontal" ? "slideleft" : "slideup";
41
+
42
+ const clips: Clip[] = childPaths.map((path, i) => {
43
+ const isVideo = path.endsWith(".mp4") || path.endsWith(".webm");
44
+ const isLast = i === childPaths.length - 1;
45
+
46
+ return {
47
+ layers: [
48
+ isVideo
49
+ ? { type: "video" as const, path, resizeMode: "cover" as const }
50
+ : { type: "image" as const, path, resizeMode: "cover" as const },
51
+ ],
52
+ duration: 3,
53
+ transition: isLast ? null : { name: transitionName, duration: 0.5 },
54
+ };
55
+ });
56
+
57
+ const outPath = `/tmp/varg-slider-${Date.now()}.mp4`;
58
+
59
+ await editly({
60
+ outPath,
61
+ width: ctx.width,
62
+ height: ctx.height,
63
+ fps: ctx.fps,
64
+ clips,
65
+ });
66
+
67
+ ctx.tempFiles.push(outPath);
68
+ return outPath;
69
+ }
@@ -0,0 +1,53 @@
1
+ import { experimental_generateSpeech as generateSpeech } from "ai";
2
+ import { File } from "../../ai-sdk/file";
3
+ import type { SpeechProps, VargElement } from "../types";
4
+ import type { RenderContext } from "./context";
5
+ import { addTask, completeTask, startTask } from "./progress";
6
+ import { computeCacheKey, getTextContent } from "./utils";
7
+
8
+ export interface SpeechResult {
9
+ path: string;
10
+ duration?: number;
11
+ }
12
+
13
+ export async function renderSpeech(
14
+ element: VargElement<"speech">,
15
+ ctx: RenderContext,
16
+ ): Promise<SpeechResult> {
17
+ const props = element.props as SpeechProps;
18
+ const text = getTextContent(element.children);
19
+
20
+ if (!text) {
21
+ throw new Error("Speech element requires text content");
22
+ }
23
+
24
+ const model = props.model;
25
+ if (!model) {
26
+ throw new Error("Speech element requires 'model' prop");
27
+ }
28
+
29
+ const cacheKey = computeCacheKey(element);
30
+
31
+ const modelId = typeof model === "string" ? model : model.modelId;
32
+ const taskId = ctx.progress ? addTask(ctx.progress, "speech", modelId) : null;
33
+ if (taskId && ctx.progress) startTask(ctx.progress, taskId);
34
+
35
+ const { audio } = await generateSpeech({
36
+ model,
37
+ text,
38
+ voice: props.voice ?? "adam",
39
+ cacheKey,
40
+ } as Parameters<typeof generateSpeech>[0]);
41
+
42
+ if (taskId && ctx.progress) completeTask(ctx.progress, taskId);
43
+
44
+ const tempPath = await File.toTemp({
45
+ uint8Array: audio.uint8Array,
46
+ mimeType: "audio/mpeg",
47
+ });
48
+ ctx.tempFiles.push(tempPath);
49
+
50
+ return {
51
+ path: tempPath,
52
+ };
53
+ }