@effectnode/media 0.5.0 → 0.7.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/LICENSE +21 -0
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
- package/dist/backend/movie-backend/agent/agent-backend.js +172 -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.d.ts +1 -1
- package/dist/backend/movie-backend/generation-queue.js +128 -9
- package/dist/backend/movie-backend/render-media.js +49 -226
- package/frontend/index.html +3 -5
- package/frontend/src/movie-app/SetupPage.tsx +1 -1
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +127 -23
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +38 -37
- package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +37 -9
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +0 -33
- package/frontend/src/movie-app/components/ProjectManager.tsx +1 -1
- package/frontend/src/movie-app/index.css +46 -20
- package/frontend/src/movie-app/index.html +1 -1
- package/frontend/src/movie-app/stores/aiModelStore.ts +20 -7
- package/frontend/src/movie-app/stores/generationStore.ts +0 -1
- package/frontend/src/movie-app/stores/movieStudioStore.ts +359 -32
- package/frontend/src/movie-app/stores/queueStore.ts +66 -1
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
|
@@ -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
|
-
const MLX_VLM_MODEL = "mlx-community/gemma-4-
|
|
32
|
-
const H3_MODEL = "appautomaton/minimax-h3-base-8bit-mlx";
|
|
30
|
+
const MLX_VLM_MODEL = "mlx-community/gemma-4-e4b-it-8bit";
|
|
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))
|
|
@@ -616,7 +614,7 @@ export async function generateSceneImage(projectId, scene, onLog) {
|
|
|
616
614
|
const fluxArgs = [mlxgen, "generate", "--model", FLUX_KLEIN_MODEL];
|
|
617
615
|
for (const p of refImages)
|
|
618
616
|
fluxArgs.push("--image", p);
|
|
619
|
-
fluxArgs.push("--prompt", String(scene?.imagePrompt || ""), "--output", sceneImagePath, "--mlx-cache-limit-gb", "20", "--steps", "5", "--seed", "42", "--width", "
|
|
617
|
+
fluxArgs.push("--prompt", String(scene?.imagePrompt || ""), "--output", sceneImagePath, "--mlx-cache-limit-gb", "20", "--steps", "5", "--seed", "42", "--width", "1024", "--height", "1024");
|
|
620
618
|
const result = await runCommand(fluxArgs, { onLog });
|
|
621
619
|
if (!result.success || !existsSync(sceneImagePath)) {
|
|
622
620
|
return { error: result.output || `Failed to generate scene image ${s}` };
|
|
@@ -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,8 +2187,8 @@ 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) => {
|
|
2190
|
+
// ========== MLX-Gen: Download Z-Image Model ==========
|
|
2191
|
+
app.post("/api/mlxgen/download-z-model", async (_req, res) => {
|
|
2195
2192
|
res.writeHead(200, {
|
|
2196
2193
|
"Content-Type": "text/event-stream",
|
|
2197
2194
|
"Cache-Control": "no-cache",
|
|
@@ -2201,13 +2198,14 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2201
2198
|
const send = (event, data) => {
|
|
2202
2199
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2203
2200
|
};
|
|
2201
|
+
const model = Z_IMAGE_MODEL;
|
|
2204
2202
|
try {
|
|
2205
2203
|
const mlxgen = await getMlxgenBin();
|
|
2206
2204
|
send("progress", {
|
|
2207
2205
|
status: "starting",
|
|
2208
|
-
label: `Downloading model ${
|
|
2206
|
+
label: `Downloading model ${model}...`,
|
|
2209
2207
|
});
|
|
2210
|
-
const proc = spawn([mlxgen, "download", "--model",
|
|
2208
|
+
const proc = spawn([mlxgen, "download", "--model", model], {
|
|
2211
2209
|
stdout: "pipe",
|
|
2212
2210
|
stderr: "pipe",
|
|
2213
2211
|
});
|
|
@@ -2234,8 +2232,8 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2234
2232
|
res.end();
|
|
2235
2233
|
}
|
|
2236
2234
|
});
|
|
2237
|
-
// ========== MLX-Gen: Download
|
|
2238
|
-
app.post("/api/mlxgen/download-
|
|
2235
|
+
// ========== MLX-Gen: Download FLUX.2 Klein Model ==========
|
|
2236
|
+
app.post("/api/mlxgen/download-flux-model", async (_req, res) => {
|
|
2239
2237
|
res.writeHead(200, {
|
|
2240
2238
|
"Content-Type": "text/event-stream",
|
|
2241
2239
|
"Cache-Control": "no-cache",
|
|
@@ -2245,14 +2243,13 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2245
2243
|
const send = (event, data) => {
|
|
2246
2244
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2247
2245
|
};
|
|
2248
|
-
const model = Z_IMAGE_MODEL;
|
|
2249
2246
|
try {
|
|
2250
2247
|
const mlxgen = await getMlxgenBin();
|
|
2251
2248
|
send("progress", {
|
|
2252
2249
|
status: "starting",
|
|
2253
|
-
label: `Downloading model ${
|
|
2250
|
+
label: `Downloading model ${FLUX_KLEIN_MODEL}...`,
|
|
2254
2251
|
});
|
|
2255
|
-
const proc = spawn([mlxgen, "download", "--model",
|
|
2252
|
+
const proc = spawn([mlxgen, "download", "--model", FLUX_KLEIN_MODEL], {
|
|
2256
2253
|
stdout: "pipe",
|
|
2257
2254
|
stderr: "pipe",
|
|
2258
2255
|
});
|
|
@@ -2279,8 +2276,20 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2279
2276
|
res.end();
|
|
2280
2277
|
}
|
|
2281
2278
|
});
|
|
2282
|
-
// ==========
|
|
2283
|
-
app.
|
|
2279
|
+
// ========== H3: Download Model ==========
|
|
2280
|
+
app.get("/api/h3/status", (_req, res) => {
|
|
2281
|
+
res.json({ downloaded: isH3ModelDownloaded() });
|
|
2282
|
+
});
|
|
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
|
+
mlxVlmDownloaded: isModelDownloaded(MLX_VLM_MODEL),
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
2292
|
+
app.post("/api/hf/install", async (_req, res) => {
|
|
2284
2293
|
res.writeHead(200, {
|
|
2285
2294
|
"Content-Type": "text/event-stream",
|
|
2286
2295
|
"Cache-Control": "no-cache",
|
|
@@ -2291,18 +2300,14 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2291
2300
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2292
2301
|
};
|
|
2293
2302
|
try {
|
|
2294
|
-
const mlxgen = await getMlxgenBin();
|
|
2295
2303
|
send("progress", {
|
|
2296
2304
|
status: "starting",
|
|
2297
|
-
label:
|
|
2298
|
-
});
|
|
2299
|
-
const proc = spawn([mlxgen, "download", "--model", FLUX_KLEIN_MODEL], {
|
|
2300
|
-
stdout: "pipe",
|
|
2301
|
-
stderr: "pipe",
|
|
2305
|
+
label: "Installing huggingface-cli...",
|
|
2302
2306
|
});
|
|
2307
|
+
const proc = spawn(["bash", "-c", "curl -LsSf https://hf.co/cli/install.sh | bash"], { stdout: "pipe", stderr: "pipe" });
|
|
2303
2308
|
activeProc = proc;
|
|
2304
|
-
const stdoutPromise = streamToSSE(proc.stdout, "
|
|
2305
|
-
const stderrText = await streamToSSE(proc.stderr, "
|
|
2309
|
+
const stdoutPromise = streamToSSE(proc.stdout, "HF Install", send);
|
|
2310
|
+
const stderrText = await streamToSSE(proc.stderr, "HF Install", send);
|
|
2306
2311
|
await stdoutPromise;
|
|
2307
2312
|
const exitCode = await proc.exited;
|
|
2308
2313
|
if (exitCode === 0) {
|
|
@@ -2323,11 +2328,7 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2323
2328
|
res.end();
|
|
2324
2329
|
}
|
|
2325
2330
|
});
|
|
2326
|
-
|
|
2327
|
-
app.get("/api/h3/status", (_req, res) => {
|
|
2328
|
-
res.json({ downloaded: isH3ModelDownloaded() });
|
|
2329
|
-
});
|
|
2330
|
-
app.post("/api/h3/download-model", async (_req, res) => {
|
|
2331
|
+
app.post("/api/hf/download-ltx", async (_req, res) => {
|
|
2331
2332
|
res.writeHead(200, {
|
|
2332
2333
|
"Content-Type": "text/event-stream",
|
|
2333
2334
|
"Cache-Control": "no-cache",
|
|
@@ -2337,25 +2338,18 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2337
2338
|
const send = (event, data) => {
|
|
2338
2339
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2339
2340
|
};
|
|
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
2341
|
try {
|
|
2347
2342
|
send("progress", {
|
|
2348
2343
|
status: "starting",
|
|
2349
|
-
label:
|
|
2344
|
+
label: "Downloading dgrauet/ltx-2.3-mlx-q8...",
|
|
2350
2345
|
});
|
|
2351
|
-
const proc = spawn(["hf", "download",
|
|
2352
|
-
cwd: featureFolder,
|
|
2346
|
+
const proc = spawn(["hf", "download", "dgrauet/ltx-2.3-mlx-q8"], {
|
|
2353
2347
|
stdout: "pipe",
|
|
2354
2348
|
stderr: "pipe",
|
|
2355
2349
|
});
|
|
2356
2350
|
activeProc = proc;
|
|
2357
|
-
const stdoutPromise = streamToSSE(proc.stdout, "
|
|
2358
|
-
const stderrText = await streamToSSE(proc.stderr, "
|
|
2351
|
+
const stdoutPromise = streamToSSE(proc.stdout, "HF Download", send);
|
|
2352
|
+
const stderrText = await streamToSSE(proc.stderr, "HF Download", send);
|
|
2359
2353
|
await stdoutPromise;
|
|
2360
2354
|
const exitCode = await proc.exited;
|
|
2361
2355
|
if (exitCode === 0) {
|
|
@@ -2376,42 +2370,7 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2376
2370
|
res.end();
|
|
2377
2371
|
}
|
|
2378
2372
|
});
|
|
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
|
|
2373
|
+
app.post("/api/hf/download-mlx-vlm", async (_req, res) => {
|
|
2415
2374
|
res.writeHead(200, {
|
|
2416
2375
|
"Content-Type": "text/event-stream",
|
|
2417
2376
|
"Cache-Control": "no-cache",
|
|
@@ -2422,66 +2381,21 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2422
2381
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2423
2382
|
};
|
|
2424
2383
|
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
2384
|
send("progress", {
|
|
2446
2385
|
status: "starting",
|
|
2447
|
-
label:
|
|
2448
|
-
outputFile,
|
|
2449
|
-
settings: {
|
|
2450
|
-
steps: stepCount,
|
|
2451
|
-
width: videoWidth,
|
|
2452
|
-
height: videoHeight,
|
|
2453
|
-
frames: frameCount,
|
|
2454
|
-
seed: seedValue,
|
|
2455
|
-
},
|
|
2386
|
+
label: `Downloading ${MLX_VLM_MODEL}...`,
|
|
2456
2387
|
});
|
|
2457
|
-
const
|
|
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,
|
|
2388
|
+
const proc = spawn(["hf", "download", MLX_VLM_MODEL], {
|
|
2470
2389
|
stdout: "pipe",
|
|
2471
2390
|
stderr: "pipe",
|
|
2472
2391
|
});
|
|
2473
2392
|
activeProc = proc;
|
|
2474
|
-
const stdoutPromise = streamToSSE(proc.stdout, "
|
|
2475
|
-
const stderrText = await streamToSSE(proc.stderr, "
|
|
2393
|
+
const stdoutPromise = streamToSSE(proc.stdout, "HF Download", send);
|
|
2394
|
+
const stderrText = await streamToSSE(proc.stderr, "HF Download", send);
|
|
2476
2395
|
await stdoutPromise;
|
|
2477
2396
|
const exitCode = await proc.exited;
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
send("complete", {
|
|
2481
|
-
success: true,
|
|
2482
|
-
path: outputPath,
|
|
2483
|
-
filename: outputFile,
|
|
2484
|
-
});
|
|
2397
|
+
if (exitCode === 0) {
|
|
2398
|
+
send("complete", { success: true });
|
|
2485
2399
|
}
|
|
2486
2400
|
else {
|
|
2487
2401
|
send("error", {
|
|
@@ -2498,48 +2412,7 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2498
2412
|
res.end();
|
|
2499
2413
|
}
|
|
2500
2414
|
});
|
|
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
|
-
}
|
|
2415
|
+
app.post("/api/hf/download-tts", async (_req, res) => {
|
|
2543
2416
|
res.writeHead(200, {
|
|
2544
2417
|
"Content-Type": "text/event-stream",
|
|
2545
2418
|
"Cache-Control": "no-cache",
|
|
@@ -2550,55 +2423,21 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2550
2423
|
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
2551
2424
|
};
|
|
2552
2425
|
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
2426
|
send("progress", {
|
|
2559
2427
|
status: "starting",
|
|
2560
|
-
label: "
|
|
2561
|
-
outputFile,
|
|
2428
|
+
label: "Downloading Qwen/Qwen3-TTS-12Hz-1.7B-Base...",
|
|
2562
2429
|
});
|
|
2563
|
-
const
|
|
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, {
|
|
2430
|
+
const proc = spawn(["hf", "download", "Qwen/Qwen3-TTS-12Hz-1.7B-Base"], {
|
|
2587
2431
|
stdout: "pipe",
|
|
2588
2432
|
stderr: "pipe",
|
|
2589
2433
|
});
|
|
2590
2434
|
activeProc = proc;
|
|
2591
|
-
const stdoutPromise = streamToSSE(proc.stdout, "
|
|
2592
|
-
const stderrText = await streamToSSE(proc.stderr, "
|
|
2435
|
+
const stdoutPromise = streamToSSE(proc.stdout, "HF Download", send);
|
|
2436
|
+
const stderrText = await streamToSSE(proc.stderr, "HF Download", send);
|
|
2593
2437
|
await stdoutPromise;
|
|
2594
2438
|
const exitCode = await proc.exited;
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
send("complete", {
|
|
2598
|
-
success: true,
|
|
2599
|
-
path: outputPath,
|
|
2600
|
-
filename: outputFile,
|
|
2601
|
-
});
|
|
2439
|
+
if (exitCode === 0) {
|
|
2440
|
+
send("complete", { success: true });
|
|
2602
2441
|
}
|
|
2603
2442
|
else {
|
|
2604
2443
|
send("error", {
|
|
@@ -2612,22 +2451,6 @@ export async function renderMediaRoutes({ app, getUvPath, }) {
|
|
|
2612
2451
|
}
|
|
2613
2452
|
finally {
|
|
2614
2453
|
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
2454
|
res.end();
|
|
2632
2455
|
}
|
|
2633
2456
|
});
|
package/frontend/index.html
CHANGED
|
@@ -7,11 +7,9 @@
|
|
|
7
7
|
<meta name="theme-color" content="#eaf8f6" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
-
<link
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/>
|
|
14
|
-
<title>Lambobo Studio</title>
|
|
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>
|
|
15
13
|
</head>
|
|
16
14
|
|
|
17
15
|
<body>
|
|
@@ -232,7 +232,7 @@ function SetupPage({ port = 8765 }) {
|
|
|
232
232
|
{/* Hero */}
|
|
233
233
|
<header>
|
|
234
234
|
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-tiffany-600">
|
|
235
|
-
|
|
235
|
+
EffectNode Media
|
|
236
236
|
</p>
|
|
237
237
|
<h1 className="mt-3 font-display text-4xl font-light leading-[1.06] text-ink-900">
|
|
238
238
|
Setting up{" "}
|