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.
- package/.claude/settings.local.json +1 -1
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +23 -0
- package/.husky/README.md +102 -0
- package/.husky/commit-msg +6 -0
- package/.husky/pre-commit +9 -0
- package/.husky/pre-push +6 -0
- package/.size-limit.json +8 -0
- package/.test-hooks.ts +5 -0
- package/CLAUDE.md +10 -3
- package/CONTRIBUTING.md +150 -0
- package/LICENSE.md +53 -0
- package/README.md +56 -209
- package/SKILLS.md +26 -10
- package/biome.json +7 -1
- package/bun.lock +1286 -0
- package/commitlint.config.js +22 -0
- package/docs/index.html +1130 -0
- package/docs/prompting.md +326 -0
- package/docs/react.md +834 -0
- package/docs/sdk.md +812 -0
- package/ffmpeg/CLAUDE.md +68 -0
- package/package.json +48 -8
- package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
- package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
- package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
- package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
- package/pipeline/cookbooks/text-to-tiktok.md +669 -0
- package/pipeline/cookbooks/trendwatching.md +156 -0
- package/plan.md +281 -0
- package/scripts/.gitkeep +0 -0
- package/src/ai-sdk/cache.ts +142 -0
- package/src/ai-sdk/examples/cached-generation.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
- package/src/ai-sdk/examples/duet-video.ts +56 -0
- package/src/ai-sdk/examples/editly-composition.ts +63 -0
- package/src/ai-sdk/examples/editly-test.ts +57 -0
- package/src/ai-sdk/examples/editly-video-test.ts +52 -0
- package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
- package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
- package/src/ai-sdk/examples/music-generation.ts +19 -0
- package/src/ai-sdk/examples/openai-sora.ts +34 -0
- package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
- package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
- package/src/ai-sdk/examples/talking-lion.ts +55 -0
- package/src/ai-sdk/examples/video-generation.ts +39 -0
- package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
- package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
- package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
- package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
- package/src/ai-sdk/file-cache.ts +112 -0
- package/src/ai-sdk/file.ts +238 -0
- package/src/ai-sdk/generate-element.ts +92 -0
- package/src/ai-sdk/generate-music.ts +46 -0
- package/src/ai-sdk/generate-video.ts +165 -0
- package/src/ai-sdk/index.ts +72 -0
- package/src/ai-sdk/music-model.ts +110 -0
- package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
- package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
- package/src/ai-sdk/providers/editly/index.ts +817 -0
- package/src/ai-sdk/providers/editly/layers.ts +776 -0
- package/src/ai-sdk/providers/editly/plan.md +144 -0
- package/src/ai-sdk/providers/editly/types.ts +328 -0
- package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
- package/src/ai-sdk/providers/fal-provider.ts +512 -0
- package/src/ai-sdk/providers/higgsfield.ts +379 -0
- package/src/ai-sdk/providers/openai.ts +251 -0
- package/src/ai-sdk/providers/replicate.ts +16 -0
- package/src/ai-sdk/video-model.ts +185 -0
- package/src/cli/commands/find.tsx +137 -0
- package/src/cli/commands/help.tsx +85 -0
- package/src/cli/commands/index.ts +6 -0
- package/src/cli/commands/list.tsx +238 -0
- package/src/cli/commands/render.tsx +71 -0
- package/src/cli/commands/run.tsx +511 -0
- package/src/cli/commands/which.tsx +253 -0
- package/src/cli/index.ts +114 -0
- package/src/cli/quiet.ts +44 -0
- package/src/cli/types.ts +32 -0
- package/src/cli/ui/components/Badge.tsx +29 -0
- package/src/cli/ui/components/DataTable.tsx +51 -0
- package/src/cli/ui/components/Header.tsx +23 -0
- package/src/cli/ui/components/HelpBlock.tsx +44 -0
- package/src/cli/ui/components/KeyValue.tsx +33 -0
- package/src/cli/ui/components/OptionRow.tsx +81 -0
- package/src/cli/ui/components/Separator.tsx +23 -0
- package/src/cli/ui/components/StatusBox.tsx +108 -0
- package/src/cli/ui/components/VargBox.tsx +51 -0
- package/src/cli/ui/components/VargProgress.tsx +36 -0
- package/src/cli/ui/components/VargSpinner.tsx +34 -0
- package/src/cli/ui/components/VargText.tsx +56 -0
- package/src/cli/ui/components/index.ts +19 -0
- package/src/cli/ui/index.ts +12 -0
- package/src/cli/ui/render.ts +35 -0
- package/src/cli/ui/theme.ts +63 -0
- package/src/cli/utils.ts +78 -0
- package/src/core/executor/executor.ts +201 -0
- package/src/core/executor/index.ts +13 -0
- package/src/core/executor/job.ts +214 -0
- package/src/core/executor/pipeline.ts +222 -0
- package/src/core/index.ts +11 -0
- package/src/core/registry/index.ts +9 -0
- package/src/core/registry/loader.ts +149 -0
- package/src/core/registry/registry.ts +221 -0
- package/src/core/registry/resolver.ts +206 -0
- package/src/core/schema/helpers.ts +134 -0
- package/src/core/schema/index.ts +8 -0
- package/src/core/schema/shared.ts +102 -0
- package/src/core/schema/types.ts +279 -0
- package/src/core/schema/validator.ts +92 -0
- package/src/definitions/actions/captions.ts +261 -0
- package/src/definitions/actions/edit.ts +298 -0
- package/src/definitions/actions/image.ts +125 -0
- package/src/definitions/actions/index.ts +114 -0
- package/src/definitions/actions/music.ts +205 -0
- package/src/definitions/actions/sync.ts +128 -0
- package/{action/transcribe/index.ts → src/definitions/actions/transcribe.ts} +63 -90
- package/src/definitions/actions/upload.ts +111 -0
- package/src/definitions/actions/video.ts +163 -0
- package/src/definitions/actions/voice.ts +119 -0
- package/src/definitions/index.ts +23 -0
- package/src/definitions/models/elevenlabs.ts +50 -0
- package/src/definitions/models/flux.ts +56 -0
- package/src/definitions/models/index.ts +36 -0
- package/src/definitions/models/kling.ts +56 -0
- package/src/definitions/models/llama.ts +54 -0
- package/src/definitions/models/nano-banana-pro.ts +102 -0
- package/src/definitions/models/sonauto.ts +68 -0
- package/src/definitions/models/soul.ts +65 -0
- package/src/definitions/models/wan.ts +54 -0
- package/src/definitions/models/whisper.ts +44 -0
- package/src/definitions/skills/index.ts +12 -0
- package/src/definitions/skills/talking-character.ts +87 -0
- package/src/definitions/skills/text-to-tiktok.ts +97 -0
- package/src/index.ts +118 -0
- package/src/providers/apify.ts +269 -0
- package/src/providers/base.ts +264 -0
- package/src/providers/elevenlabs.ts +217 -0
- package/src/providers/fal.ts +392 -0
- package/src/providers/ffmpeg.ts +544 -0
- package/src/providers/fireworks.ts +193 -0
- package/src/providers/groq.ts +149 -0
- package/src/providers/higgsfield.ts +145 -0
- package/src/providers/index.ts +143 -0
- package/src/providers/replicate.ts +147 -0
- package/src/providers/storage.ts +206 -0
- package/src/react/cli.ts +52 -0
- package/src/react/elements.ts +146 -0
- package/src/react/examples/branching.tsx +66 -0
- package/src/react/examples/captions-demo.tsx +37 -0
- package/src/react/examples/character-video.tsx +84 -0
- package/src/react/examples/grid.tsx +53 -0
- package/src/react/examples/layouts-demo.tsx +57 -0
- package/src/react/examples/madi.tsx +60 -0
- package/src/react/examples/music-test.tsx +35 -0
- package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
- package/src/react/examples/orange-portrait.tsx +41 -0
- package/src/react/examples/split-element-demo.tsx +60 -0
- package/src/react/examples/split-layout-demo.tsx +60 -0
- package/src/react/examples/split.tsx +41 -0
- package/src/react/examples/video-grid.tsx +46 -0
- package/src/react/index.ts +43 -0
- package/src/react/layouts/grid.tsx +28 -0
- package/src/react/layouts/index.ts +2 -0
- package/src/react/layouts/split.tsx +20 -0
- package/src/react/react.test.ts +309 -0
- package/src/react/render.ts +21 -0
- package/src/react/renderers/animate.ts +59 -0
- package/src/react/renderers/captions.ts +297 -0
- package/src/react/renderers/clip.ts +248 -0
- package/src/react/renderers/context.ts +17 -0
- package/src/react/renderers/image.ts +109 -0
- package/src/react/renderers/index.ts +22 -0
- package/src/react/renderers/music.ts +60 -0
- package/src/react/renderers/packshot.ts +84 -0
- package/src/react/renderers/progress.ts +173 -0
- package/src/react/renderers/render.ts +243 -0
- package/src/react/renderers/slider.ts +69 -0
- package/src/react/renderers/speech.ts +53 -0
- package/src/react/renderers/split.ts +91 -0
- package/src/react/renderers/subtitle.ts +16 -0
- package/src/react/renderers/swipe.ts +75 -0
- package/src/react/renderers/title.ts +17 -0
- package/src/react/renderers/utils.ts +124 -0
- package/src/react/renderers/video.ts +127 -0
- package/src/react/runtime/jsx-dev-runtime.ts +43 -0
- package/src/react/runtime/jsx-runtime.ts +35 -0
- package/src/react/types.ts +232 -0
- package/src/studio/index.ts +26 -0
- package/src/studio/scanner.ts +102 -0
- package/src/studio/server.ts +554 -0
- package/src/studio/stages.ts +251 -0
- package/src/studio/step-renderer.ts +279 -0
- package/src/studio/types.ts +60 -0
- package/src/studio/ui/cache.html +303 -0
- package/src/studio/ui/index.html +1820 -0
- package/src/tests/all.test.ts +509 -0
- package/src/tests/index.ts +33 -0
- package/src/tests/unit.test.ts +403 -0
- package/tsconfig.cli.json +8 -0
- package/tsconfig.json +21 -3
- package/TEST_RESULTS.md +0 -122
- package/action/captions/SKILL.md +0 -170
- package/action/captions/index.ts +0 -227
- package/action/edit/SKILL.md +0 -235
- package/action/edit/index.ts +0 -493
- package/action/image/SKILL.md +0 -140
- package/action/image/index.ts +0 -112
- package/action/sync/SKILL.md +0 -136
- package/action/sync/index.ts +0 -187
- package/action/transcribe/SKILL.md +0 -179
- package/action/video/SKILL.md +0 -116
- package/action/video/index.ts +0 -135
- package/action/voice/SKILL.md +0 -125
- package/action/voice/index.ts +0 -201
- package/index.ts +0 -38
- package/lib/README.md +0 -144
- package/lib/ai-sdk/fal.ts +0 -106
- package/lib/ai-sdk/replicate.ts +0 -107
- package/lib/elevenlabs.ts +0 -382
- package/lib/fal.ts +0 -478
- package/lib/ffmpeg.ts +0 -467
- package/lib/fireworks.ts +0 -235
- package/lib/groq.ts +0 -246
- package/lib/higgsfield.ts +0 -176
- package/lib/remotion/SKILL.md +0 -823
- package/lib/remotion/cli.ts +0 -115
- package/lib/remotion/functions.ts +0 -283
- package/lib/remotion/index.ts +0 -19
- package/lib/remotion/templates.ts +0 -73
- package/lib/replicate.ts +0 -304
- package/output.txt +0 -1
- package/test-import.ts +0 -7
- package/test-services.ts +0 -97
- 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
|
+
}
|