varg.ai-sdk 0.1.0 → 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 (236) 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 +48 -8
  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} +63 -90
  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 -227
  206. package/action/edit/SKILL.md +0 -235
  207. package/action/edit/index.ts +0 -493
  208. package/action/image/SKILL.md +0 -140
  209. package/action/image/index.ts +0 -112
  210. package/action/sync/SKILL.md +0 -136
  211. package/action/sync/index.ts +0 -187
  212. package/action/transcribe/SKILL.md +0 -179
  213. package/action/video/SKILL.md +0 -116
  214. package/action/video/index.ts +0 -135
  215. package/action/voice/SKILL.md +0 -125
  216. package/action/voice/index.ts +0 -201
  217. package/index.ts +0 -38
  218. package/lib/README.md +0 -144
  219. package/lib/ai-sdk/fal.ts +0 -106
  220. package/lib/ai-sdk/replicate.ts +0 -107
  221. package/lib/elevenlabs.ts +0 -382
  222. package/lib/fal.ts +0 -478
  223. package/lib/ffmpeg.ts +0 -467
  224. package/lib/fireworks.ts +0 -235
  225. package/lib/groq.ts +0 -246
  226. package/lib/higgsfield.ts +0 -176
  227. package/lib/remotion/SKILL.md +0 -823
  228. package/lib/remotion/cli.ts +0 -115
  229. package/lib/remotion/functions.ts +0 -283
  230. package/lib/remotion/index.ts +0 -19
  231. package/lib/remotion/templates.ts +0 -73
  232. package/lib/replicate.ts +0 -304
  233. package/output.txt +0 -1
  234. package/test-import.ts +0 -7
  235. package/test-services.ts +0 -97
  236. package/utilities/s3.ts +0 -147
@@ -0,0 +1,554 @@
1
+ import { existsSync, mkdirSync, readdirSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { getCacheItemMedia, scanCacheFolder } from "./scanner";
4
+ import { extractStages, serializeStages } from "./stages";
5
+ import {
6
+ createStepSession,
7
+ deleteSession,
8
+ executeNextStage,
9
+ executeStage,
10
+ getSession,
11
+ getSessionStatus,
12
+ getStagePreviewPath,
13
+ } from "./step-renderer";
14
+ import type { CacheItem, RenderProgress, RenderRequest } from "./types";
15
+
16
+ const DEFAULT_CACHE_DIR = ".cache/ai";
17
+ const DEFAULT_OUTPUT_DIR = "output/studio";
18
+ const DEFAULT_SHARES_DIR = "output/studio/shares";
19
+
20
+ interface StudioConfig {
21
+ cacheDir: string;
22
+ outputDir: string;
23
+ port: number;
24
+ initialFile?: string;
25
+ }
26
+
27
+ interface ShareData {
28
+ code: string;
29
+ videoUrl?: string;
30
+ createdAt: string;
31
+ }
32
+
33
+ interface TemplateInfo {
34
+ id: string;
35
+ name: string;
36
+ filename: string;
37
+ }
38
+
39
+ function fileNameToReadable(filename: string): string {
40
+ return basename(filename, ".tsx")
41
+ .replace(/[-_]/g, " ")
42
+ .replace(/\b\w/g, (c) => c.toLowerCase());
43
+ }
44
+
45
+ function scanTemplates(dir: string): TemplateInfo[] {
46
+ const templates: TemplateInfo[] = [];
47
+
48
+ if (!existsSync(dir)) return templates;
49
+
50
+ const entries = readdirSync(dir, { withFileTypes: true });
51
+
52
+ for (const entry of entries) {
53
+ if (
54
+ entry.isFile() &&
55
+ entry.name.endsWith(".tsx") &&
56
+ !entry.name.startsWith("_")
57
+ ) {
58
+ const id = basename(entry.name, ".tsx");
59
+ templates.push({
60
+ id,
61
+ name: fileNameToReadable(entry.name),
62
+ filename: entry.name,
63
+ });
64
+ }
65
+ }
66
+
67
+ return templates.sort((a, b) => a.name.localeCompare(b.name));
68
+ }
69
+
70
+ export function createStudioServer(config: Partial<StudioConfig> = {}) {
71
+ const cacheDir = resolve(config.cacheDir ?? DEFAULT_CACHE_DIR);
72
+ const outputDir = resolve(config.outputDir ?? DEFAULT_OUTPUT_DIR);
73
+ const sharesDir = resolve(DEFAULT_SHARES_DIR);
74
+ const port = config.port ?? 8282;
75
+ const initialFile = config.initialFile;
76
+
77
+ if (!existsSync(outputDir)) {
78
+ mkdirSync(outputDir, { recursive: true });
79
+ }
80
+ if (!existsSync(sharesDir)) {
81
+ mkdirSync(sharesDir, { recursive: true });
82
+ }
83
+
84
+ let cachedItems: CacheItem[] = [];
85
+
86
+ async function refreshCache() {
87
+ if (existsSync(cacheDir)) {
88
+ cachedItems = await scanCacheFolder(cacheDir);
89
+ }
90
+ }
91
+
92
+ const activeRenders = new Map<string, AbortController>();
93
+
94
+ async function executeRender(
95
+ code: string,
96
+ renderId: string,
97
+ onProgress: (progress: RenderProgress) => void,
98
+ _signal: AbortSignal,
99
+ ): Promise<string> {
100
+ const outputPath = join(outputDir, `${renderId}.mp4`);
101
+ const tempDir = join(import.meta.dir, "../react/examples");
102
+ const tempFile = join(tempDir, `_studio_${renderId}.tsx`);
103
+
104
+ onProgress({ step: "parsing", progress: 0, message: "parsing code..." });
105
+ await Bun.write(tempFile, code);
106
+ onProgress({
107
+ step: "rendering",
108
+ progress: 0.1,
109
+ message: "starting render...",
110
+ });
111
+
112
+ try {
113
+ console.log(`[render] importing ${tempFile}`);
114
+ const mod = await import(tempFile);
115
+ const element = mod.default;
116
+
117
+ if (
118
+ !element ||
119
+ typeof element !== "object" ||
120
+ element.type !== "render"
121
+ ) {
122
+ throw new Error("file must export a <Render> element as default");
123
+ }
124
+
125
+ console.log("[render] starting render pipeline");
126
+ const { render } = await import("../react/render");
127
+
128
+ await render(element, {
129
+ output: outputPath,
130
+ cache: cacheDir,
131
+ quiet: false,
132
+ });
133
+
134
+ console.log(`[render] complete: ${outputPath}`);
135
+ onProgress({ step: "complete", progress: 1, message: "done!" });
136
+ return outputPath;
137
+ } catch (err) {
138
+ const msg = err instanceof Error ? err.message : String(err);
139
+ console.error(`[render] error: ${msg}`);
140
+ if (err instanceof Error && err.stack) {
141
+ console.error(err.stack);
142
+ }
143
+ throw new Error(msg);
144
+ } finally {
145
+ try {
146
+ if (await Bun.file(tempFile).exists()) {
147
+ await Bun.$`rm ${tempFile}`;
148
+ }
149
+ } catch {}
150
+ }
151
+ }
152
+
153
+ const server = Bun.serve({
154
+ port,
155
+ idleTimeout: 0,
156
+ async fetch(req) {
157
+ const url = new URL(req.url);
158
+ const method = req.method;
159
+ const path = url.pathname;
160
+
161
+ if (path.startsWith("/api/")) {
162
+ console.log(`${method} ${path}`);
163
+ }
164
+
165
+ if (url.pathname === "/" || url.pathname === "/editor") {
166
+ const html = await Bun.file(
167
+ join(import.meta.dir, "ui/index.html"),
168
+ ).text();
169
+ return new Response(html, { headers: { "content-type": "text/html" } });
170
+ }
171
+
172
+ if (url.pathname === "/cache") {
173
+ const html = await Bun.file(
174
+ join(import.meta.dir, "ui/cache.html"),
175
+ ).text();
176
+ return new Response(html, { headers: { "content-type": "text/html" } });
177
+ }
178
+
179
+ if (url.pathname === "/api/items") {
180
+ await refreshCache();
181
+ return Response.json(cachedItems);
182
+ }
183
+
184
+ if (url.pathname.startsWith("/api/media/")) {
185
+ const id = decodeURIComponent(url.pathname.replace("/api/media/", ""));
186
+ const media = await getCacheItemMedia(cacheDir, id);
187
+ if (!media) return new Response("not found", { status: 404 });
188
+ const buffer = Buffer.from(media.data, "base64");
189
+ return new Response(buffer, {
190
+ headers: { "content-type": media.mimeType },
191
+ });
192
+ }
193
+
194
+ if (url.pathname === "/api/initial-code") {
195
+ if (!initialFile) {
196
+ return Response.json({ code: null });
197
+ }
198
+ const file = Bun.file(initialFile);
199
+ if (!(await file.exists())) {
200
+ return Response.json({ code: null, error: "file not found" });
201
+ }
202
+ const code = await file.text();
203
+ return Response.json({ code, path: initialFile });
204
+ }
205
+
206
+ if (url.pathname === "/api/templates") {
207
+ const examplesDir = join(import.meta.dir, "../react/examples");
208
+ const templates = scanTemplates(examplesDir).map((t) => ({
209
+ id: t.id,
210
+ name: t.name,
211
+ }));
212
+ return Response.json(templates);
213
+ }
214
+
215
+ if (url.pathname.startsWith("/api/templates/")) {
216
+ const id = url.pathname.replace("/api/templates/", "");
217
+ const examplesDir = join(import.meta.dir, "../react/examples");
218
+ const templates = scanTemplates(examplesDir);
219
+ const template = templates.find((t) => t.id === id);
220
+ if (!template) return new Response("not found", { status: 404 });
221
+ const code = await Bun.file(
222
+ join(examplesDir, template.filename),
223
+ ).text();
224
+ return Response.json({ code });
225
+ }
226
+
227
+ if (url.pathname === "/api/render" && req.method === "POST") {
228
+ const body = (await req.json()) as RenderRequest;
229
+ const renderId = `render-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
230
+ const controller = new AbortController();
231
+ activeRenders.set(renderId, controller);
232
+
233
+ const stream = new ReadableStream({
234
+ async start(streamController) {
235
+ const encoder = new TextEncoder();
236
+ const send = (event: string, data: unknown) => {
237
+ streamController.enqueue(
238
+ encoder.encode(
239
+ `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`,
240
+ ),
241
+ );
242
+ };
243
+
244
+ try {
245
+ send("start", { renderId });
246
+ const _outputPath = await executeRender(
247
+ body.code,
248
+ renderId,
249
+ (progress) => send("progress", progress),
250
+ controller.signal,
251
+ );
252
+ send("complete", { videoUrl: `/api/output/${renderId}.mp4` });
253
+ } catch (error) {
254
+ const message =
255
+ error instanceof Error ? error.message : "unknown error";
256
+ send("error", { message });
257
+ } finally {
258
+ activeRenders.delete(renderId);
259
+ streamController.close();
260
+ }
261
+ },
262
+ });
263
+
264
+ return new Response(stream, {
265
+ headers: {
266
+ "content-type": "text/event-stream",
267
+ "cache-control": "no-cache",
268
+ connection: "keep-alive",
269
+ },
270
+ });
271
+ }
272
+
273
+ if (url.pathname.startsWith("/api/render/") && req.method === "DELETE") {
274
+ const renderId = url.pathname.replace("/api/render/", "");
275
+ const controller = activeRenders.get(renderId);
276
+ if (controller) {
277
+ controller.abort();
278
+ activeRenders.delete(renderId);
279
+ return Response.json({ stopped: true });
280
+ }
281
+ return Response.json({ stopped: false });
282
+ }
283
+
284
+ if (url.pathname.startsWith("/api/output/")) {
285
+ const filename = url.pathname.replace("/api/output/", "");
286
+ const filePath = join(outputDir, filename);
287
+ const file = Bun.file(filePath);
288
+ if (!(await file.exists()))
289
+ return new Response("not found", { status: 404 });
290
+ return new Response(file, { headers: { "content-type": "video/mp4" } });
291
+ }
292
+
293
+ if (url.pathname === "/api/share" && req.method === "POST") {
294
+ const body = (await req.json()) as { code: string; videoUrl?: string };
295
+ const shareId = Math.random().toString(36).slice(2, 10);
296
+ const shareData: ShareData = {
297
+ code: body.code,
298
+ videoUrl: body.videoUrl,
299
+ createdAt: new Date().toISOString(),
300
+ };
301
+ await Bun.write(
302
+ join(sharesDir, `${shareId}.json`),
303
+ JSON.stringify(shareData),
304
+ );
305
+ return Response.json({ shareId, url: `/s/${shareId}` });
306
+ }
307
+
308
+ if (url.pathname.startsWith("/api/share/")) {
309
+ const shareId = url.pathname.replace("/api/share/", "");
310
+ const sharePath = join(sharesDir, `${shareId}.json`);
311
+ const file = Bun.file(sharePath);
312
+ if (!(await file.exists()))
313
+ return new Response("not found", { status: 404 });
314
+ const data = await file.json();
315
+ return Response.json(data);
316
+ }
317
+
318
+ if (url.pathname.startsWith("/s/")) {
319
+ const html = await Bun.file(
320
+ join(import.meta.dir, "ui/index.html"),
321
+ ).text();
322
+ return new Response(html, { headers: { "content-type": "text/html" } });
323
+ }
324
+
325
+ if (url.pathname === "/api/step/stages" && req.method === "POST") {
326
+ const body = (await req.json()) as { code: string };
327
+ const tempDir = join(import.meta.dir, "../react/examples");
328
+ const tempFile = join(tempDir, `_stages_${Date.now()}.tsx`);
329
+
330
+ try {
331
+ await Bun.write(tempFile, body.code);
332
+ const mod = await import(tempFile);
333
+ const element = mod.default;
334
+
335
+ if (
336
+ !element ||
337
+ typeof element !== "object" ||
338
+ element.type !== "render"
339
+ ) {
340
+ return Response.json(
341
+ { error: "file must export a <Render> element as default" },
342
+ { status: 400 },
343
+ );
344
+ }
345
+
346
+ const extracted = extractStages(element);
347
+ const serialized = serializeStages(extracted);
348
+
349
+ return Response.json(serialized);
350
+ } catch (err) {
351
+ const message = err instanceof Error ? err.message : String(err);
352
+ return Response.json({ error: message }, { status: 400 });
353
+ } finally {
354
+ try {
355
+ if (await Bun.file(tempFile).exists()) {
356
+ await Bun.$`rm ${tempFile}`;
357
+ }
358
+ } catch {}
359
+ }
360
+ }
361
+
362
+ if (url.pathname === "/api/step/session" && req.method === "POST") {
363
+ const body = (await req.json()) as { code: string };
364
+ const tempDir = join(import.meta.dir, "../react/examples");
365
+ const tempFile = join(tempDir, `_session_${Date.now()}.tsx`);
366
+
367
+ try {
368
+ await Bun.write(tempFile, body.code);
369
+ const mod = await import(tempFile);
370
+ const element = mod.default;
371
+
372
+ if (
373
+ !element ||
374
+ typeof element !== "object" ||
375
+ element.type !== "render"
376
+ ) {
377
+ return Response.json(
378
+ { error: "file must export a <Render> element as default" },
379
+ { status: 400 },
380
+ );
381
+ }
382
+
383
+ const session = createStepSession(body.code, element, cacheDir);
384
+ const status = getSessionStatus(session);
385
+
386
+ return Response.json(status);
387
+ } catch (err) {
388
+ const message = err instanceof Error ? err.message : String(err);
389
+ return Response.json({ error: message }, { status: 400 });
390
+ } finally {
391
+ try {
392
+ if (await Bun.file(tempFile).exists()) {
393
+ await Bun.$`rm ${tempFile}`;
394
+ }
395
+ } catch {}
396
+ }
397
+ }
398
+
399
+ if (url.pathname === "/api/step/next" && req.method === "POST") {
400
+ const body = (await req.json()) as { sessionId: string };
401
+ const session = getSession(body.sessionId);
402
+
403
+ if (!session) {
404
+ return Response.json({ error: "session not found" }, { status: 404 });
405
+ }
406
+
407
+ try {
408
+ const result = await executeNextStage(session);
409
+
410
+ if (!result) {
411
+ return Response.json({
412
+ done: true,
413
+ status: getSessionStatus(session),
414
+ });
415
+ }
416
+
417
+ return Response.json({
418
+ done: false,
419
+ stage: {
420
+ id: result.stage.id,
421
+ type: result.stage.type,
422
+ label: result.stage.label,
423
+ status: result.stage.status,
424
+ },
425
+ result: result.result,
426
+ isLast: result.isLast,
427
+ status: getSessionStatus(session),
428
+ });
429
+ } catch (err) {
430
+ const message = err instanceof Error ? err.message : String(err);
431
+ return Response.json(
432
+ { error: message, status: getSessionStatus(session) },
433
+ { status: 500 },
434
+ );
435
+ }
436
+ }
437
+
438
+ if (url.pathname === "/api/step/run" && req.method === "POST") {
439
+ const body = (await req.json()) as {
440
+ sessionId: string;
441
+ stageId: string;
442
+ };
443
+ const session = getSession(body.sessionId);
444
+
445
+ if (!session) {
446
+ return Response.json({ error: "session not found" }, { status: 404 });
447
+ }
448
+
449
+ try {
450
+ const result = await executeStage(session, body.stageId);
451
+ return Response.json({
452
+ result,
453
+ status: getSessionStatus(session),
454
+ });
455
+ } catch (err) {
456
+ const message = err instanceof Error ? err.message : String(err);
457
+ return Response.json(
458
+ { error: message, status: getSessionStatus(session) },
459
+ { status: 500 },
460
+ );
461
+ }
462
+ }
463
+
464
+ if (
465
+ url.pathname.match(/^\/api\/step\/preview\/[^/]+\/[^/]+$/) &&
466
+ req.method === "GET"
467
+ ) {
468
+ const parts = url.pathname.split("/");
469
+ const sessionId = parts[4];
470
+ const stageId = parts[5];
471
+
472
+ if (!sessionId || !stageId) {
473
+ return new Response("invalid path", { status: 400 });
474
+ }
475
+
476
+ const session = getSession(sessionId);
477
+ if (!session) {
478
+ return new Response("session not found", { status: 404 });
479
+ }
480
+
481
+ const previewPath = getStagePreviewPath(session, stageId);
482
+ if (!previewPath) {
483
+ return new Response("preview not found", { status: 404 });
484
+ }
485
+
486
+ const file = Bun.file(previewPath);
487
+ if (!(await file.exists())) {
488
+ return new Response("file not found", { status: 404 });
489
+ }
490
+
491
+ const mimeType = previewPath.endsWith(".mp4")
492
+ ? "video/mp4"
493
+ : previewPath.endsWith(".mp3")
494
+ ? "audio/mp3"
495
+ : "image/png";
496
+
497
+ return new Response(file, { headers: { "content-type": mimeType } });
498
+ }
499
+
500
+ if (
501
+ url.pathname.match(/^\/api\/step\/session\/[^/]+$/) &&
502
+ req.method === "GET"
503
+ ) {
504
+ const sessionId = url.pathname.split("/").pop();
505
+ if (!sessionId) {
506
+ return new Response("invalid path", { status: 400 });
507
+ }
508
+
509
+ const session = getSession(sessionId);
510
+ if (!session) {
511
+ return Response.json({ error: "session not found" }, { status: 404 });
512
+ }
513
+
514
+ return Response.json(getSessionStatus(session));
515
+ }
516
+
517
+ if (
518
+ url.pathname.match(/^\/api\/step\/session\/[^/]+$/) &&
519
+ req.method === "DELETE"
520
+ ) {
521
+ const sessionId = url.pathname.split("/").pop();
522
+ if (!sessionId) {
523
+ return new Response("invalid path", { status: 400 });
524
+ }
525
+
526
+ deleteSession(sessionId);
527
+ return Response.json({ deleted: true });
528
+ }
529
+
530
+ if (url.pathname === "/api/step/render" && req.method === "POST") {
531
+ const body = (await req.json()) as { sessionId: string };
532
+ const session = getSession(body.sessionId);
533
+
534
+ if (!session) {
535
+ return Response.json({ error: "session not found" }, { status: 404 });
536
+ }
537
+
538
+ try {
539
+ const { finalizeRender } = await import("./step-renderer");
540
+ const videoPath = await finalizeRender(session, outputDir);
541
+ const videoUrl = `/api/output/${videoPath.split("/").pop()}`;
542
+ return Response.json({ videoUrl, path: videoPath });
543
+ } catch (err) {
544
+ const message = err instanceof Error ? err.message : String(err);
545
+ return Response.json({ error: message }, { status: 500 });
546
+ }
547
+ }
548
+
549
+ return new Response("not found", { status: 404 });
550
+ },
551
+ });
552
+
553
+ return { server, port };
554
+ }