@effectnode/media 0.4.0 → 0.6.0
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/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
- package/dist/backend/movie-backend/agent/agent-backend.js +169 -83
- package/dist/backend/movie-backend/agent/tools/index.js +0 -2
- package/dist/backend/movie-backend/core.js +1 -1
- package/dist/backend/movie-backend/generation-queue.js +107 -44
- package/dist/backend/movie-backend/render-media.js +28 -254
- package/frontend/index.html +6 -1
- package/frontend/public/lambobo.png +0 -0
- package/frontend/src/movie-app/MediaStudio.tsx +6 -2
- package/frontend/src/movie-app/SetupPage.tsx +98 -63
- package/frontend/src/movie-app/components/Aurora.tsx +13 -0
- package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +5 -8
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +417 -205
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +350 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -67
- package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
- package/frontend/src/movie-app/index.css +275 -8
- package/frontend/src/movie-app/index.html +1 -1
- package/frontend/src/movie-app/stores/aiModelStore.ts +172 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2 -3
- package/frontend/src/movie-app/stores/movieStudioStore.ts +72 -3
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
- package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +0 -881
|
@@ -25,11 +25,9 @@ const PYTHON_DIR = join(APP_DATA_DIR, "python-src");
|
|
|
25
25
|
const TEMP_DIR = join(APP_DATA_DIR, "temp");
|
|
26
26
|
const PROJECTS_FILE = join(JSON_DIR, "projects.json");
|
|
27
27
|
const CHARACTERS_FILE = join(JSON_DIR, "characters.json");
|
|
28
|
-
const MLXGEN_MODEL = "AbstractFramework/qwen-image-edit-2511-8bit";
|
|
29
28
|
const Z_IMAGE_MODEL = "AbstractFramework/z-image-turbo-8bit";
|
|
30
29
|
const FLUX_KLEIN_MODEL = "AbstractFramework/flux.2-klein-4b-8bit";
|
|
31
30
|
const MLX_VLM_MODEL = "mlx-community/gemma-4-e2b-it-4bit";
|
|
32
|
-
const H3_MODEL = "appautomaton/minimax-h3-base-8bit-mlx";
|
|
33
31
|
const VIDEO_STAGE_FLAGS = {
|
|
34
32
|
distilled: "--distilled",
|
|
35
33
|
"one-stage": "--one-stage",
|
|
@@ -330,7 +328,7 @@ function huggingfaceCacheDir() {
|
|
|
330
328
|
return join(homedir(), ".cache", "huggingface", "hub");
|
|
331
329
|
}
|
|
332
330
|
/** True when the given MLX-Gen model has already been downloaded to the HF cache. */
|
|
333
|
-
function isModelDownloaded(model
|
|
331
|
+
function isModelDownloaded(model) {
|
|
334
332
|
const modelDirName = `models--${model.replace("/", "--")}`;
|
|
335
333
|
const snapshotsDir = join(huggingfaceCacheDir(), modelDirName, "snapshots");
|
|
336
334
|
if (!existsSync(snapshotsDir))
|
|
@@ -2141,7 +2139,6 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2141
2139
|
app.get("/api/mlxgen/status", (_req, res) => {
|
|
2142
2140
|
res.json({
|
|
2143
2141
|
installed: isMlxgenInstalled(),
|
|
2144
|
-
modelDownloaded: isModelDownloaded(),
|
|
2145
2142
|
zModelDownloaded: isModelDownloaded(Z_IMAGE_MODEL),
|
|
2146
2143
|
fluxModelDownloaded: isModelDownloaded(FLUX_KLEIN_MODEL),
|
|
2147
2144
|
});
|
|
@@ -2190,50 +2187,6 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2190
2187
|
res.end();
|
|
2191
2188
|
}
|
|
2192
2189
|
});
|
|
2193
|
-
// ========== MLX-Gen: Download Model ==========
|
|
2194
|
-
app.post("/api/mlxgen/download-model", async (_req, res) => {
|
|
2195
|
-
res.writeHead(200, {
|
|
2196
|
-
"Content-Type": "text/event-stream",
|
|
2197
|
-
"Cache-Control": "no-cache",
|
|
2198
|
-
Connection: "keep-alive",
|
|
2199
|
-
"X-Accel-Buffering": "no",
|
|
2200
|
-
});
|
|
2201
|
-
const send = (event, data) => {
|
|
2202
|
-
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2203
|
-
};
|
|
2204
|
-
try {
|
|
2205
|
-
const mlxgen = await getMlxgenBin();
|
|
2206
|
-
send("progress", {
|
|
2207
|
-
status: "starting",
|
|
2208
|
-
label: `Downloading model ${MLXGEN_MODEL}...`,
|
|
2209
|
-
});
|
|
2210
|
-
const proc = spawn([mlxgen, "download", "--model", MLXGEN_MODEL], {
|
|
2211
|
-
stdout: "pipe",
|
|
2212
|
-
stderr: "pipe",
|
|
2213
|
-
});
|
|
2214
|
-
activeProc = proc;
|
|
2215
|
-
const stdoutPromise = streamToSSE(proc.stdout, "Download", send);
|
|
2216
|
-
const stderrText = await streamToSSE(proc.stderr, "Download", send);
|
|
2217
|
-
await stdoutPromise;
|
|
2218
|
-
const exitCode = await proc.exited;
|
|
2219
|
-
if (exitCode === 0) {
|
|
2220
|
-
send("complete", { success: true });
|
|
2221
|
-
}
|
|
2222
|
-
else {
|
|
2223
|
-
send("error", {
|
|
2224
|
-
error: stderrText || `Process exited with code ${exitCode}`,
|
|
2225
|
-
exitCode,
|
|
2226
|
-
});
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
catch (e) {
|
|
2230
|
-
send("error", { error: String(e) });
|
|
2231
|
-
}
|
|
2232
|
-
finally {
|
|
2233
|
-
activeProc = null;
|
|
2234
|
-
res.end();
|
|
2235
|
-
}
|
|
2236
|
-
});
|
|
2237
2190
|
// ========== MLX-Gen: Download Z-Image Model ==========
|
|
2238
2191
|
app.post("/api/mlxgen/download-z-model", async (_req, res) => {
|
|
2239
2192
|
res.writeHead(200, {
|
|
@@ -2327,7 +2280,15 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2327
2280
|
app.get("/api/h3/status", (_req, res) => {
|
|
2328
2281
|
res.json({ downloaded: isH3ModelDownloaded() });
|
|
2329
2282
|
});
|
|
2330
|
-
|
|
2283
|
+
// ========== Hugging Face CLI + LTX Video Model ==========
|
|
2284
|
+
app.get("/api/hf/status", (_req, res) => {
|
|
2285
|
+
res.json({
|
|
2286
|
+
installed: whichSync("hf") !== null,
|
|
2287
|
+
ltxDownloaded: isModelDownloaded("dgrauet/ltx-2.3-mlx-q8"),
|
|
2288
|
+
ttsDownloaded: isModelDownloaded("Qwen/Qwen3-TTS-12Hz-1.7B-Base"),
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
app.post("/api/hf/install", async (_req, res) => {
|
|
2331
2292
|
res.writeHead(200, {
|
|
2332
2293
|
"Content-Type": "text/event-stream",
|
|
2333
2294
|
"Cache-Control": "no-cache",
|
|
@@ -2337,25 +2298,15 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2337
2298
|
const send = (event, data) => {
|
|
2338
2299
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2339
2300
|
};
|
|
2340
|
-
// The `hf download` command must run from inside the mlx-h3 checkout so
|
|
2341
|
-
// its weights land in <mlx-h3>/weights.
|
|
2342
|
-
const featureFolder = join(PYTHON_DIR, "mlx-h3");
|
|
2343
|
-
if (!existsSync(featureFolder)) {
|
|
2344
|
-
mkdirSync(featureFolder, { recursive: true });
|
|
2345
|
-
}
|
|
2346
2301
|
try {
|
|
2347
2302
|
send("progress", {
|
|
2348
2303
|
status: "starting",
|
|
2349
|
-
label:
|
|
2350
|
-
});
|
|
2351
|
-
const proc = spawn(["hf", "download", H3_MODEL, "--local-dir", "weights"], {
|
|
2352
|
-
cwd: featureFolder,
|
|
2353
|
-
stdout: "pipe",
|
|
2354
|
-
stderr: "pipe",
|
|
2304
|
+
label: "Installing huggingface-cli...",
|
|
2355
2305
|
});
|
|
2306
|
+
const proc = spawn(["bash", "-c", "curl -LsSf https://hf.co/cli/install.sh | bash"], { stdout: "pipe", stderr: "pipe" });
|
|
2356
2307
|
activeProc = proc;
|
|
2357
|
-
const stdoutPromise = streamToSSE(proc.stdout, "
|
|
2358
|
-
const stderrText = await streamToSSE(proc.stderr, "
|
|
2308
|
+
const stdoutPromise = streamToSSE(proc.stdout, "HF Install", send);
|
|
2309
|
+
const stderrText = await streamToSSE(proc.stderr, "HF Install", send);
|
|
2359
2310
|
await stdoutPromise;
|
|
2360
2311
|
const exitCode = await proc.exited;
|
|
2361
2312
|
if (exitCode === 0) {
|
|
@@ -2376,42 +2327,7 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2376
2327
|
res.end();
|
|
2377
2328
|
}
|
|
2378
2329
|
});
|
|
2379
|
-
|
|
2380
|
-
app.post("/api/h3/generate", async (req, res) => {
|
|
2381
|
-
const { prompt, refs, projectId, steps = 20, width = 640, height = 448, frames = 121, seed = 42, } = req.body || {};
|
|
2382
|
-
if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
|
|
2383
|
-
res.status(400).json({ error: "Prompt is required" });
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
if (!projectId || !isValidProjectId(String(projectId))) {
|
|
2387
|
-
res.status(400).json({ error: "Invalid project ID" });
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
// Resolve ordered reference media (images, videos, and audio) to safe paths.
|
|
2391
|
-
const mediaRefs = Array.isArray(refs)
|
|
2392
|
-
? refs.filter((r) => !!r &&
|
|
2393
|
-
(r.kind === "image" || r.kind === "video" || r.kind === "audio") &&
|
|
2394
|
-
typeof r.filename === "string" &&
|
|
2395
|
-
!!r.filename.trim())
|
|
2396
|
-
: [];
|
|
2397
|
-
if (mediaRefs.length === 0) {
|
|
2398
|
-
res.status(400).json({
|
|
2399
|
-
error: "At least one reference image, video, or audio is required. Upload or select one first.",
|
|
2400
|
-
});
|
|
2401
|
-
return;
|
|
2402
|
-
}
|
|
2403
|
-
const resolvedRefs = [];
|
|
2404
|
-
for (const ref of mediaRefs) {
|
|
2405
|
-
const resolved = resolveSafePath(ref.filename.trim(), String(projectId));
|
|
2406
|
-
if (!resolved) {
|
|
2407
|
-
res.status(400).json({
|
|
2408
|
-
error: `Reference ${ref.kind} not found in this project: ${ref.filename}`,
|
|
2409
|
-
});
|
|
2410
|
-
return;
|
|
2411
|
-
}
|
|
2412
|
-
resolvedRefs.push({ kind: ref.kind, path: resolved });
|
|
2413
|
-
}
|
|
2414
|
-
// SSE headers
|
|
2330
|
+
app.post("/api/hf/download-ltx", async (_req, res) => {
|
|
2415
2331
|
res.writeHead(200, {
|
|
2416
2332
|
"Content-Type": "text/event-stream",
|
|
2417
2333
|
"Cache-Control": "no-cache",
|
|
@@ -2422,66 +2338,18 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2422
2338
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2423
2339
|
};
|
|
2424
2340
|
try {
|
|
2425
|
-
const featureFolder = join(PYTHON_DIR, "mlx-h3");
|
|
2426
|
-
if (!existsSync(featureFolder)) {
|
|
2427
|
-
send("error", { error: "mlx-h3 not found. Run setup first." });
|
|
2428
|
-
res.end();
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
const uvPath = await getUvPath();
|
|
2432
|
-
const projectOutputDir = resolveOutputDir(null, String(projectId));
|
|
2433
|
-
if (!projectOutputDir) {
|
|
2434
|
-
send("error", { error: "Invalid output directory." });
|
|
2435
|
-
res.end();
|
|
2436
|
-
return;
|
|
2437
|
-
}
|
|
2438
|
-
const outputFile = `references-${Date.now()}.mp4`;
|
|
2439
|
-
const outputPath = join(projectOutputDir, outputFile);
|
|
2440
|
-
const stepCount = Number(steps) || 20;
|
|
2441
|
-
const videoWidth = Number(width) || 640;
|
|
2442
|
-
const videoHeight = Number(height) || 448;
|
|
2443
|
-
const frameCount = Number(frames) || 121;
|
|
2444
|
-
const seedValue = Number(seed) || 42;
|
|
2445
2341
|
send("progress", {
|
|
2446
2342
|
status: "starting",
|
|
2447
|
-
label: "
|
|
2448
|
-
outputFile,
|
|
2449
|
-
settings: {
|
|
2450
|
-
steps: stepCount,
|
|
2451
|
-
width: videoWidth,
|
|
2452
|
-
height: videoHeight,
|
|
2453
|
-
frames: frameCount,
|
|
2454
|
-
seed: seedValue,
|
|
2455
|
-
},
|
|
2456
|
-
});
|
|
2457
|
-
const args = [uvPath, "run", "mlx-h3"];
|
|
2458
|
-
for (const ref of resolvedRefs) {
|
|
2459
|
-
const flag = ref.kind === "image"
|
|
2460
|
-
? "--ref-image"
|
|
2461
|
-
: ref.kind === "video"
|
|
2462
|
-
? "--ref-video"
|
|
2463
|
-
: "--ref-audio";
|
|
2464
|
-
args.push(flag, ref.path);
|
|
2465
|
-
}
|
|
2466
|
-
args.push("--steps", String(stepCount), "--width", String(videoWidth), "--height", String(videoHeight), "--frames", String(frameCount), "--seed", String(seedValue), "--output", outputPath, prompt);
|
|
2467
|
-
const proc = spawn(args, {
|
|
2468
|
-
env: process.env,
|
|
2469
|
-
cwd: featureFolder,
|
|
2470
|
-
stdout: "pipe",
|
|
2471
|
-
stderr: "pipe",
|
|
2343
|
+
label: "Downloading dgrauet/ltx-2.3-mlx-q8...",
|
|
2472
2344
|
});
|
|
2345
|
+
const proc = spawn(["hf", "download", "dgrauet/ltx-2.3-mlx-q8"], { stdout: "pipe", stderr: "pipe" });
|
|
2473
2346
|
activeProc = proc;
|
|
2474
|
-
const stdoutPromise = streamToSSE(proc.stdout, "
|
|
2475
|
-
const stderrText = await streamToSSE(proc.stderr, "
|
|
2347
|
+
const stdoutPromise = streamToSSE(proc.stdout, "HF Download", send);
|
|
2348
|
+
const stderrText = await streamToSSE(proc.stderr, "HF Download", send);
|
|
2476
2349
|
await stdoutPromise;
|
|
2477
2350
|
const exitCode = await proc.exited;
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
send("complete", {
|
|
2481
|
-
success: true,
|
|
2482
|
-
path: outputPath,
|
|
2483
|
-
filename: outputFile,
|
|
2484
|
-
});
|
|
2351
|
+
if (exitCode === 0) {
|
|
2352
|
+
send("complete", { success: true });
|
|
2485
2353
|
}
|
|
2486
2354
|
else {
|
|
2487
2355
|
send("error", {
|
|
@@ -2498,48 +2366,7 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2498
2366
|
res.end();
|
|
2499
2367
|
}
|
|
2500
2368
|
});
|
|
2501
|
-
|
|
2502
|
-
app.post("/api/mlxgen/generate", async (req, res) => {
|
|
2503
|
-
const { prompt, imagePath, image, projectId, width, height } = req.body || {};
|
|
2504
|
-
if (!prompt) {
|
|
2505
|
-
res.status(400).json({ error: "Prompt is required" });
|
|
2506
|
-
return;
|
|
2507
|
-
}
|
|
2508
|
-
if (!projectId) {
|
|
2509
|
-
res.status(400).json({ error: "Project ID is required" });
|
|
2510
|
-
return;
|
|
2511
|
-
}
|
|
2512
|
-
// Project IDs are generated by makeId() (alphanumeric + hyphen). Reject
|
|
2513
|
-
// anything else (including `.`, `..`, and path separators) so a malicious
|
|
2514
|
-
// projectId cannot escape OUTPUT_DIR/TEMP_DIR via `../`.
|
|
2515
|
-
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(String(projectId))) {
|
|
2516
|
-
res.status(400).json({ error: "Invalid project ID" });
|
|
2517
|
-
return;
|
|
2518
|
-
}
|
|
2519
|
-
// Resolve the character image. Prefer a client-side preprocessed PNG
|
|
2520
|
-
// (base64), which is written to the temp workspace; otherwise fall back to
|
|
2521
|
-
// a bare filename previously uploaded to this project.
|
|
2522
|
-
let resolvedImage = null;
|
|
2523
|
-
let tempImagePath = null;
|
|
2524
|
-
if (image) {
|
|
2525
|
-
// Decode base64 (strip data URL prefix if present)
|
|
2526
|
-
const base64 = String(image).replace(/^data:image\/\w+;base64,/, "");
|
|
2527
|
-
const buffer = Buffer.from(base64, "base64");
|
|
2528
|
-
const tempDir = join(TEMP_DIR, String(projectId));
|
|
2529
|
-
ensureDir(tempDir);
|
|
2530
|
-
tempImagePath = join(tempDir, `temp-${Date.now()}.png`);
|
|
2531
|
-
writeFileSync(tempImagePath, buffer);
|
|
2532
|
-
resolvedImage = tempImagePath;
|
|
2533
|
-
}
|
|
2534
|
-
else if (imagePath) {
|
|
2535
|
-
resolvedImage = resolveSafePath(imagePath, projectId);
|
|
2536
|
-
}
|
|
2537
|
-
if (!resolvedImage) {
|
|
2538
|
-
res.status(400).json({
|
|
2539
|
-
error: "Image is required. Provide a preprocessed image or a filename previously uploaded to this project.",
|
|
2540
|
-
});
|
|
2541
|
-
return;
|
|
2542
|
-
}
|
|
2369
|
+
app.post("/api/hf/download-tts", async (_req, res) => {
|
|
2543
2370
|
res.writeHead(200, {
|
|
2544
2371
|
"Content-Type": "text/event-stream",
|
|
2545
2372
|
"Cache-Control": "no-cache",
|
|
@@ -2550,55 +2377,18 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2550
2377
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2551
2378
|
};
|
|
2552
2379
|
try {
|
|
2553
|
-
const mlxgen = await getMlxgenBin();
|
|
2554
|
-
const projectOutputDir = join(OUTPUT_DIR, projectId);
|
|
2555
|
-
ensureDir(projectOutputDir);
|
|
2556
|
-
const outputFile = `mlxgen-${Date.now()}.png`;
|
|
2557
|
-
const outputPath = join(projectOutputDir, outputFile);
|
|
2558
2380
|
send("progress", {
|
|
2559
2381
|
status: "starting",
|
|
2560
|
-
label: "
|
|
2561
|
-
outputFile,
|
|
2562
|
-
});
|
|
2563
|
-
const args = [
|
|
2564
|
-
mlxgen,
|
|
2565
|
-
"generate",
|
|
2566
|
-
"--model",
|
|
2567
|
-
MLXGEN_MODEL,
|
|
2568
|
-
"--image",
|
|
2569
|
-
resolvedImage,
|
|
2570
|
-
"--prompt",
|
|
2571
|
-
prompt,
|
|
2572
|
-
"--output",
|
|
2573
|
-
outputPath,
|
|
2574
|
-
"--steps",
|
|
2575
|
-
String(40),
|
|
2576
|
-
];
|
|
2577
|
-
// Optional output size (kept in the same aspect ratio by the client).
|
|
2578
|
-
const outWidth = Number(width);
|
|
2579
|
-
const outHeight = Number(height);
|
|
2580
|
-
if (Number.isInteger(outWidth) &&
|
|
2581
|
-
outWidth > 0 &&
|
|
2582
|
-
Number.isInteger(outHeight) &&
|
|
2583
|
-
outHeight > 0) {
|
|
2584
|
-
args.push("--width", String(outWidth), "--height", String(outHeight));
|
|
2585
|
-
}
|
|
2586
|
-
const proc = spawn(args, {
|
|
2587
|
-
stdout: "pipe",
|
|
2588
|
-
stderr: "pipe",
|
|
2382
|
+
label: "Downloading Qwen/Qwen3-TTS-12Hz-1.7B-Base...",
|
|
2589
2383
|
});
|
|
2384
|
+
const proc = spawn(["hf", "download", "Qwen/Qwen3-TTS-12Hz-1.7B-Base"], { stdout: "pipe", stderr: "pipe" });
|
|
2590
2385
|
activeProc = proc;
|
|
2591
|
-
const stdoutPromise = streamToSSE(proc.stdout, "
|
|
2592
|
-
const stderrText = await streamToSSE(proc.stderr, "
|
|
2386
|
+
const stdoutPromise = streamToSSE(proc.stdout, "HF Download", send);
|
|
2387
|
+
const stderrText = await streamToSSE(proc.stderr, "HF Download", send);
|
|
2593
2388
|
await stdoutPromise;
|
|
2594
2389
|
const exitCode = await proc.exited;
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
send("complete", {
|
|
2598
|
-
success: true,
|
|
2599
|
-
path: outputPath,
|
|
2600
|
-
filename: outputFile,
|
|
2601
|
-
});
|
|
2390
|
+
if (exitCode === 0) {
|
|
2391
|
+
send("complete", { success: true });
|
|
2602
2392
|
}
|
|
2603
2393
|
else {
|
|
2604
2394
|
send("error", {
|
|
@@ -2612,22 +2402,6 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2612
2402
|
}
|
|
2613
2403
|
finally {
|
|
2614
2404
|
activeProc = null;
|
|
2615
|
-
// Clean up the temporary workspace image now that generation is done.
|
|
2616
|
-
if (tempImagePath) {
|
|
2617
|
-
try {
|
|
2618
|
-
unlinkSync(tempImagePath);
|
|
2619
|
-
}
|
|
2620
|
-
catch {
|
|
2621
|
-
// already removed
|
|
2622
|
-
}
|
|
2623
|
-
try {
|
|
2624
|
-
// Remove the temp dir only when it is empty (no recursive delete).
|
|
2625
|
-
rmSync(join(TEMP_DIR, String(projectId)), { force: true });
|
|
2626
|
-
}
|
|
2627
|
-
catch {
|
|
2628
|
-
// ignore cleanup failures
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
2405
|
res.end();
|
|
2632
2406
|
}
|
|
2633
2407
|
});
|
package/frontend/index.html
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<
|
|
7
|
+
<meta name="theme-color" content="#eaf8f6" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,300..700;1,300..600&display=swap"
|
|
11
|
+
rel="stylesheet" />
|
|
12
|
+
<title>EffectNode Media</title>
|
|
8
13
|
</head>
|
|
9
14
|
|
|
10
15
|
<body>
|
|
Binary file
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import Aurora from "./components/Aurora";
|
|
1
2
|
import ProjectManager from "./components/ProjectManager";
|
|
2
3
|
|
|
3
4
|
export default function MediaStudio() {
|
|
4
5
|
return (
|
|
5
|
-
<div className="
|
|
6
|
-
<
|
|
6
|
+
<div className="relative min-h-screen">
|
|
7
|
+
<Aurora />
|
|
8
|
+
<div className="relative z-10 mx-auto w-full max-w-4xl px-6 py-12 md:py-16">
|
|
9
|
+
<ProjectManager />
|
|
10
|
+
</div>
|
|
7
11
|
</div>
|
|
8
12
|
);
|
|
9
13
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState, useRef } from "react";
|
|
2
2
|
import { useNavigate } from "react-router-dom";
|
|
3
3
|
import { useLogStore, type LogEntry } from "./stores/logStore";
|
|
4
|
+
import Aurora from "./components/Aurora";
|
|
4
5
|
|
|
5
6
|
interface StepStatus {
|
|
6
7
|
step: string;
|
|
@@ -104,7 +105,7 @@ function SetupPage({ port = 8765 }) {
|
|
|
104
105
|
height="18"
|
|
105
106
|
viewBox="0 0 24 24"
|
|
106
107
|
fill="none"
|
|
107
|
-
stroke="
|
|
108
|
+
stroke="#4fc3bc"
|
|
108
109
|
strokeWidth="2"
|
|
109
110
|
>
|
|
110
111
|
<circle
|
|
@@ -131,7 +132,7 @@ function SetupPage({ port = 8765 }) {
|
|
|
131
132
|
height="18"
|
|
132
133
|
viewBox="0 0 24 24"
|
|
133
134
|
fill="none"
|
|
134
|
-
stroke="#
|
|
135
|
+
stroke="#0abab5"
|
|
135
136
|
strokeWidth="2"
|
|
136
137
|
>
|
|
137
138
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -145,7 +146,7 @@ function SetupPage({ port = 8765 }) {
|
|
|
145
146
|
height="18"
|
|
146
147
|
viewBox="0 0 24 24"
|
|
147
148
|
fill="none"
|
|
148
|
-
stroke="#
|
|
149
|
+
stroke="#fb7185"
|
|
149
150
|
strokeWidth="2"
|
|
150
151
|
>
|
|
151
152
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -159,7 +160,7 @@ function SetupPage({ port = 8765 }) {
|
|
|
159
160
|
height="18"
|
|
160
161
|
viewBox="0 0 24 24"
|
|
161
162
|
fill="none"
|
|
162
|
-
stroke="#
|
|
163
|
+
stroke="#83a0a7"
|
|
163
164
|
strokeWidth="2"
|
|
164
165
|
>
|
|
165
166
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -177,7 +178,7 @@ function SetupPage({ port = 8765 }) {
|
|
|
177
178
|
height="14"
|
|
178
179
|
viewBox="0 0 24 24"
|
|
179
180
|
fill="none"
|
|
180
|
-
stroke="#
|
|
181
|
+
stroke="#fb7185"
|
|
181
182
|
strokeWidth="2"
|
|
182
183
|
className="shrink-0"
|
|
183
184
|
>
|
|
@@ -192,13 +193,13 @@ function SetupPage({ port = 8765 }) {
|
|
|
192
193
|
height="14"
|
|
193
194
|
viewBox="0 0 24 24"
|
|
194
195
|
fill="none"
|
|
195
|
-
stroke="#
|
|
196
|
+
stroke="#fbbf24"
|
|
196
197
|
strokeWidth="2"
|
|
197
198
|
className="shrink-0"
|
|
198
199
|
>
|
|
199
200
|
<path d="M12 2L2 22h20L12 2z" />
|
|
200
201
|
<line x1="12" y1="9" x2="12" y2="14" />
|
|
201
|
-
<circle cx="12" cy="18" r="0.5" fill="#
|
|
202
|
+
<circle cx="12" cy="18" r="0.5" fill="#fbbf24" />
|
|
202
203
|
</svg>
|
|
203
204
|
);
|
|
204
205
|
default:
|
|
@@ -208,7 +209,7 @@ function SetupPage({ port = 8765 }) {
|
|
|
208
209
|
height="14"
|
|
209
210
|
viewBox="0 0 24 24"
|
|
210
211
|
fill="none"
|
|
211
|
-
stroke="#
|
|
212
|
+
stroke="#5e7c83"
|
|
212
213
|
strokeWidth="2"
|
|
213
214
|
className="shrink-0"
|
|
214
215
|
>
|
|
@@ -225,65 +226,99 @@ function SetupPage({ port = 8765 }) {
|
|
|
225
226
|
};
|
|
226
227
|
|
|
227
228
|
return (
|
|
228
|
-
<div className="
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
<div className="relative min-h-screen">
|
|
230
|
+
<Aurora />
|
|
231
|
+
<div className="relative z-10 mx-auto w-full max-w-3xl px-6 py-12">
|
|
232
|
+
{/* Hero */}
|
|
233
|
+
<header>
|
|
234
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-tiffany-600">
|
|
235
|
+
EffectNode Media
|
|
236
|
+
</p>
|
|
237
|
+
<h1 className="mt-3 font-display text-4xl font-light leading-[1.06] text-ink-900">
|
|
238
|
+
Setting up{" "}
|
|
239
|
+
<span className="wordmark font-medium italic">your studio</span>
|
|
240
|
+
</h1>
|
|
241
|
+
<p className="mt-4 text-sm leading-relaxed text-ink-600/80">
|
|
242
|
+
Gathering models and starting the local server. The first run takes
|
|
243
|
+
a minute.
|
|
244
|
+
</p>
|
|
245
|
+
</header>
|
|
232
246
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
247
|
+
{error && (
|
|
248
|
+
<div className="mt-8 rounded-2xl border border-rose-200 bg-rose-50/70 px-4 py-3 text-sm text-rose-600 backdrop-blur-sm">
|
|
249
|
+
Error: {error}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
{complete && !error && (
|
|
253
|
+
<div className="glass mt-8 rounded-2xl px-4 py-3 text-sm font-medium text-tiffany-700 shadow-card">
|
|
254
|
+
Setup complete — opening your studio…
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
{/* Checks */}
|
|
259
|
+
<div className="glass mt-8 rounded-3xl p-6 shadow-card">
|
|
260
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-ink-500">
|
|
261
|
+
Checks
|
|
262
|
+
</p>
|
|
263
|
+
<div className="mt-2">
|
|
264
|
+
{steps.length === 0 ? (
|
|
265
|
+
<p className="py-2 text-sm italic text-ink-500/70">
|
|
266
|
+
Starting the local server…
|
|
267
|
+
</p>
|
|
268
|
+
) : (
|
|
269
|
+
steps.map((step, i) => (
|
|
270
|
+
<div
|
|
271
|
+
key={i}
|
|
272
|
+
className="flex items-center gap-2.5 py-2 transition-opacity"
|
|
273
|
+
style={{ opacity: step.status === "pending" ? 0.45 : 1 }}
|
|
274
|
+
>
|
|
275
|
+
<StatusIcon status={step.status} />
|
|
276
|
+
<span className="text-sm text-ink-800">{step.label}</span>
|
|
277
|
+
</div>
|
|
278
|
+
))
|
|
279
|
+
)}
|
|
248
280
|
</div>
|
|
249
|
-
))}
|
|
250
|
-
</div>
|
|
251
|
-
{/* Log display panel */}
|
|
252
|
-
<div className="flex-1 flex flex-col min-h-0 border border-ink-200 rounded-2xl overflow-hidden bg-white shadow-card">
|
|
253
|
-
<div className="flex items-center justify-between px-4 py-2.5 bg-ink-50 border-b border-ink-200">
|
|
254
|
-
<span className="text-xs font-semibold text-ink-600/80 uppercase tracking-wider">
|
|
255
|
-
Logs
|
|
256
|
-
</span>
|
|
257
|
-
<span className="text-xs text-ink-500">{logs.length} entries</span>
|
|
258
281
|
</div>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
: "text-ink-700"
|
|
279
|
-
}
|
|
282
|
+
|
|
283
|
+
{/* Log console — the film-editing-bay contrast inside the dream */}
|
|
284
|
+
<div className="glass-dark mt-5 flex min-h-0 flex-col overflow-hidden rounded-3xl shadow-card">
|
|
285
|
+
<div className="flex items-center justify-between border-b border-white/10 px-5 py-3">
|
|
286
|
+
<span className="text-[11px] font-semibold uppercase tracking-[0.2em] text-tiffany-300/80">
|
|
287
|
+
Logs
|
|
288
|
+
</span>
|
|
289
|
+
<span className="text-xs text-ink-400">
|
|
290
|
+
{logs.length} {logs.length === 1 ? "entry" : "entries"}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
<div className="flex-1 overflow-y-auto p-5 font-mono text-xs leading-relaxed">
|
|
294
|
+
{logs.length === 0 ? (
|
|
295
|
+
<div className="italic text-ink-400">Waiting for logs…</div>
|
|
296
|
+
) : (
|
|
297
|
+
logs.map((entry) => (
|
|
298
|
+
<div
|
|
299
|
+
key={entry.id}
|
|
300
|
+
className="flex items-start gap-2 rounded px-1 py-0.5 hover:bg-white/5"
|
|
280
301
|
>
|
|
281
|
-
{entry.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
302
|
+
<LogIcon level={entry.level} />
|
|
303
|
+
<span className="shrink-0 select-none text-ink-400">
|
|
304
|
+
{formatTime(entry.timestamp)}
|
|
305
|
+
</span>
|
|
306
|
+
<span
|
|
307
|
+
className={
|
|
308
|
+
entry.level === "error"
|
|
309
|
+
? "text-rose-300"
|
|
310
|
+
: entry.level === "warn"
|
|
311
|
+
? "text-amber-300"
|
|
312
|
+
: "text-tiffany-100"
|
|
313
|
+
}
|
|
314
|
+
>
|
|
315
|
+
{entry.message}
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
))
|
|
319
|
+
)}
|
|
320
|
+
<div ref={logEndRef} />
|
|
321
|
+
</div>
|
|
287
322
|
</div>
|
|
288
323
|
</div>
|
|
289
324
|
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The signature atmosphere: a slow-drifting aurora of tiffany, periwinkle,
|
|
2
|
+
// blush and sky behind the frosted-glass interface. Purely decorative and
|
|
3
|
+
// pointer-transparent; animation is disabled for users who prefer reduced motion.
|
|
4
|
+
export default function Aurora() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="aurora" aria-hidden="true">
|
|
7
|
+
<div className="aurora-blob aurora-blob--tiffany" />
|
|
8
|
+
<div className="aurora-blob aurora-blob--periwinkle" />
|
|
9
|
+
<div className="aurora-blob aurora-blob--blush" />
|
|
10
|
+
<div className="aurora-blob aurora-blob--sky" />
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|