@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.
@@ -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-e2b-it-4bit";
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 = MLXGEN_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", "448", "--height", "796");
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 ${MLXGEN_MODEL}...`,
2206
+ label: `Downloading model ${model}...`,
2209
2207
  });
2210
- const proc = spawn([mlxgen, "download", "--model", MLXGEN_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 Z-Image Model ==========
2238
- app.post("/api/mlxgen/download-z-model", async (_req, res) => {
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 ${model}...`,
2250
+ label: `Downloading model ${FLUX_KLEIN_MODEL}...`,
2254
2251
  });
2255
- const proc = spawn([mlxgen, "download", "--model", 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
- // ========== MLX-Gen: Download FLUX.2 Klein Model ==========
2283
- app.post("/api/mlxgen/download-flux-model", async (_req, res) => {
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: `Downloading model ${FLUX_KLEIN_MODEL}...`,
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, "Download", send);
2305
- const stderrText = await streamToSSE(proc.stderr, "Download", send);
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
- // ========== H3: Download Model ==========
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: `Downloading model ${H3_MODEL}...`,
2344
+ label: "Downloading dgrauet/ltx-2.3-mlx-q8...",
2350
2345
  });
2351
- const proc = spawn(["hf", "download", H3_MODEL, "--local-dir", "weights"], {
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, "H3 Download", send);
2358
- const stderrText = await streamToSSE(proc.stderr, "H3 Download", send);
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
- // ========== H3: Generate (References-to-Video) ==========
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: "Generating references-to-video...",
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 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,
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, "H3 Generate", send);
2475
- const stderrText = await streamToSSE(proc.stderr, "H3 Generate", send);
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
- const success = exitCode === 0 && existsSync(outputPath);
2479
- if (success) {
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
- // ========== MLX-Gen: Generate (Image Edit) ==========
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: "Generating image...",
2561
- outputFile,
2428
+ label: "Downloading Qwen/Qwen3-TTS-12Hz-1.7B-Base...",
2562
2429
  });
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, {
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, "MLXGen", send);
2592
- const stderrText = await streamToSSE(proc.stderr, "MLXGen", send);
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
- const success = exitCode === 0 && existsSync(outputPath);
2596
- if (success) {
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
  });
@@ -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
- href="https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,300..700;1,300..600&display=swap"
12
- rel="stylesheet"
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
- Lambobo Studio
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{" "}