@effectnode/media 0.5.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.
@@ -191,9 +191,14 @@ const DOWNLOAD_MODELS: {
191
191
  desc: "Character, place & scene images",
192
192
  },
193
193
  {
194
- id: "qwen",
195
- name: "AbstractFramework/qwen-image-edit-2511-8bit",
196
- desc: "Image editing",
194
+ id: "ltx",
195
+ name: "dgrauet/ltx-2.3-mlx-q8",
196
+ desc: "Video generation",
197
+ },
198
+ {
199
+ id: "tts",
200
+ name: "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
201
+ desc: "Voice-over & speech",
197
202
  },
198
203
  ];
199
204
 
@@ -228,7 +233,7 @@ export default function SetupAiModelTab() {
228
233
  <section>
229
234
  <h3 className="mb-2 text-sm font-semibold text-ink-900">Engines</h3>
230
235
  <div className="space-y-2">
231
- <ModelRow name="mlx-gen" desc="Image generation engine (z-image · flux · qwen)">
236
+ <ModelRow name="mlx-gen" desc="Image generation engine (z-image · flux)">
232
237
  <StatusBadge
233
238
  value={store.mlxgenInstalled}
234
239
  okText="Installed"
@@ -259,6 +264,22 @@ export default function SetupAiModelTab() {
259
264
  {store.downloading === "mlx-vlm" ? "Installing…" : "Install"}
260
265
  </button>
261
266
  </ModelRow>
267
+
268
+ <ModelRow name="huggingface-cli" desc="HF CLI for downloading models">
269
+ <StatusBadge
270
+ value={store.hfInstalled}
271
+ okText="Installed"
272
+ missingText="Not installed"
273
+ />
274
+ <button
275
+ onClick={() => store.installTool("hf-cli")}
276
+ disabled={busy}
277
+ className="flex items-center gap-1.5 whitespace-nowrap rounded-xl border border-tiffany-500 px-3 py-1.5 text-xs font-medium text-tiffany-700 transition-colors hover:bg-tiffany-50 disabled:opacity-50"
278
+ >
279
+ {store.downloading === "hf-cli" ? SpinnerIcon : InstallIcon}
280
+ {store.downloading === "hf-cli" ? "Installing…" : "Install"}
281
+ </button>
282
+ </ModelRow>
262
283
  </div>
263
284
  </section>
264
285
 
@@ -286,7 +307,9 @@ export default function SetupAiModelTab() {
286
307
  ? store.zImageDownloaded
287
308
  : m.id === "flux"
288
309
  ? store.fluxDownloaded
289
- : store.qwenDownloaded;
310
+ : m.id === "ltx"
311
+ ? store.ltxDownloaded
312
+ : store.ttsDownloaded;
290
313
  return (
291
314
  <ModelRow key={m.id} name={m.name} desc={m.desc}>
292
315
  <StatusBadge value={downloaded} />
@@ -301,24 +324,6 @@ export default function SetupAiModelTab() {
301
324
  </ModelRow>
302
325
  );
303
326
  })}
304
-
305
- <ModelRow
306
- name="dgrauet/ltx-2.3-mlx-q8"
307
- desc="Video generation"
308
- >
309
- <span className="text-xs italic text-ink-400">
310
- Downloads on first render
311
- </span>
312
- </ModelRow>
313
-
314
- <ModelRow
315
- name="Qwen/Qwen3-TTS-12Hz-1.7B-Base"
316
- desc="Voice-over & speech"
317
- >
318
- <span className="text-xs italic text-ink-400">
319
- Downloads on first use
320
- </span>
321
- </ModelRow>
322
327
  </div>
323
328
  </section>
324
329
 
@@ -8,7 +8,6 @@ import MovieStudioTab from "./EditorTabs/MovieStudioTab";
8
8
  import GenerateVideoTab from "./EditorTabs/GenerateVideoTab";
9
9
  import AgentTab from "./EditorTabs/AgentTab";
10
10
  import StoryWriterTab from "./EditorTabs/StoryWriterTab";
11
- import CharactersTab from "./EditorTabs/CharactersTab";
12
11
  import ExtractImageTab from "./EditorTabs/ExtractImageTab";
13
12
  import SceneVisualTab from "./EditorTabs/SceneVisualTab";
14
13
  import TextToImageTab from "./EditorTabs/TextToImageTab";
@@ -173,22 +172,6 @@ export default function ProjectEditorPage() {
173
172
  </svg>
174
173
  );
175
174
 
176
- const CharacterIcon = (
177
- <svg
178
- width="18"
179
- height="18"
180
- viewBox="0 0 24 24"
181
- fill="none"
182
- stroke="currentColor"
183
- strokeWidth="2"
184
- strokeLinecap="round"
185
- strokeLinejoin="round"
186
- >
187
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
188
- <circle cx="12" cy="7" r="4" />
189
- </svg>
190
- );
191
-
192
175
  const ExtractIcon = (
193
176
  <svg
194
177
  width="18"
@@ -471,17 +454,6 @@ export default function ProjectEditorPage() {
471
454
  {StoryWriterIcon}
472
455
  Story Writer
473
456
  </button>
474
- <button
475
- onClick={() => store.setActiveTab("characters")}
476
- className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
477
- store.activeTab === "characters"
478
- ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
479
- : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
480
- }`}
481
- >
482
- {CharacterIcon}
483
- Characters
484
- </button>
485
457
  <button
486
458
  onClick={() => store.setActiveTab("extract")}
487
459
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
@@ -597,11 +569,6 @@ export default function ProjectEditorPage() {
597
569
  <StoryWriterTab projectId={id!} />
598
570
  )}
599
571
 
600
- {/* ========== CHARACTERS PANEL ========== */}
601
- {store.activeTab === "characters" && (
602
- <CharactersTab projectId={id!} />
603
- )}
604
-
605
572
  {/* ========== EXTRACT IMAGE PANEL ========== */}
606
573
  {store.activeTab === "extract" && <ExtractImageTab projectId={id!} />}
607
574
 
@@ -168,7 +168,7 @@ export default function ProjectManager() {
168
168
  <header className="flex flex-col gap-7 md:flex-row md:items-end md:justify-between">
169
169
  <div className="max-w-xl">
170
170
  <p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-tiffany-600">
171
- Lambobo Studio
171
+ EffectNode Media
172
172
  </p>
173
173
  <h1 className="mt-3 font-display text-4xl font-light leading-[1.06] text-ink-900 md:text-5xl">
174
174
  Make the movies{" "}
@@ -1,18 +1,20 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  /* ==========================================================================
4
- Lambobo Studio — design tokens
4
+ EffectNode Media — design tokens
5
5
  A dreamy, tiffany-blue film studio. Gradients read like light through
6
6
  shallow water: tiffany teal → aqua → periwinkle → blush.
7
7
  ========================================================================== */
8
8
 
9
9
  @theme {
10
10
  /* ---- Type ---- */
11
- --font-sans: "Avenir Next", Avenir, "Helvetica Neue", -apple-system,
12
- BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
13
- --font-display: "Cormorant", "Iowan Old Style", "Baskerville", "Georgia",
14
- serif;
15
- --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
11
+ --font-sans:
12
+ "Avenir Next", Avenir, "Helvetica Neue", -apple-system, BlinkMacSystemFont,
13
+ "Segoe UI", Roboto, sans-serif;
14
+ --font-display:
15
+ "Cormorant", "Iowan Old Style", "Baskerville", "Georgia", serif;
16
+ --font-mono:
17
+ ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
16
18
  "Liberation Mono", monospace;
17
19
 
18
20
  /* ---- Tiffany (the brand) ---- */
@@ -48,13 +50,14 @@
48
50
  --color-dream-mint: #c8f1e6;
49
51
 
50
52
  /* ---- Shadows ---- */
51
- --shadow-glow: 0 8px 30px -6px rgba(10, 186, 181, 0.55),
53
+ --shadow-glow:
54
+ 0 8px 30px -6px rgba(10, 186, 181, 0.55),
52
55
  0 2px 10px -2px rgba(10, 186, 181, 0.4);
53
56
  --shadow-glow-sm: 0 0 14px -2px rgba(10, 186, 181, 0.45);
54
- --shadow-card: 0 1px 2px rgba(13, 25, 28, 0.04),
55
- 0 10px 30px -12px rgba(13, 25, 28, 0.16);
56
- --shadow-card-hover: 0 2px 4px rgba(13, 25, 28, 0.05),
57
- 0 18px 44px -14px rgba(10, 186, 181, 0.35);
57
+ --shadow-card:
58
+ 0 1px 2px rgba(13, 25, 28, 0.04), 0 10px 30px -12px rgba(13, 25, 28, 0.16);
59
+ --shadow-card-hover:
60
+ 0 2px 4px rgba(13, 25, 28, 0.05), 0 18px 44px -14px rgba(10, 186, 181, 0.35);
58
61
  --shadow-modal: 0 24px 80px -16px rgba(13, 25, 28, 0.35);
59
62
  }
60
63
 
@@ -65,16 +68,28 @@
65
68
  body {
66
69
  margin: 0;
67
70
  background-color: #f3f7f7;
68
- background-image: radial-gradient(
71
+ background-image:
72
+ radial-gradient(
69
73
  120% 90% at 85% -10%,
70
74
  rgba(183, 225, 251, 0.5),
71
75
  transparent 60%
72
76
  ),
73
- radial-gradient(100% 80% at 0% 0%, rgba(255, 214, 234, 0.4), transparent 55%);
77
+ radial-gradient(
78
+ 100% 80% at 0% 0%,
79
+ rgba(255, 214, 234, 0.4),
80
+ transparent 55%
81
+ );
74
82
  background-attachment: fixed;
75
83
  color: #283a40;
76
- font-family: "Avenir Next", Avenir, "Helvetica Neue", -apple-system,
77
- BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
84
+ font-family:
85
+ "Avenir Next",
86
+ Avenir,
87
+ "Helvetica Neue",
88
+ -apple-system,
89
+ BlinkMacSystemFont,
90
+ "Segoe UI",
91
+ Roboto,
92
+ sans-serif;
78
93
  -webkit-font-smoothing: antialiased;
79
94
  -moz-osx-font-smoothing: grayscale;
80
95
  }
@@ -82,7 +97,8 @@ body {
82
97
  code,
83
98
  pre,
84
99
  .font-mono {
85
- font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
100
+ font-family:
101
+ ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
86
102
  "Liberation Mono", monospace;
87
103
  }
88
104
 
@@ -126,7 +142,8 @@ pre,
126
142
  z-index: 0;
127
143
  overflow: hidden;
128
144
  pointer-events: none;
129
- background: radial-gradient(
145
+ background:
146
+ radial-gradient(
130
147
  120% 90% at 85% -10%,
131
148
  rgba(183, 225, 251, 0.55),
132
149
  transparent 60%
@@ -251,7 +268,12 @@ pre,
251
268
  }
252
269
 
253
270
  .thumb {
254
- background-image: linear-gradient(135deg, #b0e8e3 0%, #81d8d0 50%, #c3b4f4 100%);
271
+ background-image: linear-gradient(
272
+ 135deg,
273
+ #b0e8e3 0%,
274
+ #81d8d0 50%,
275
+ #c3b4f4 100%
276
+ );
255
277
  }
256
278
 
257
279
  .btn-primary {
@@ -263,12 +285,16 @@ pre,
263
285
  );
264
286
  color: #ffffff;
265
287
  box-shadow: var(--shadow-glow);
266
- transition: filter 150ms ease, box-shadow 150ms ease, transform 150ms ease;
288
+ transition:
289
+ filter 150ms ease,
290
+ box-shadow 150ms ease,
291
+ transform 150ms ease;
267
292
  }
268
293
 
269
294
  .btn-primary:hover {
270
295
  filter: brightness(1.05);
271
- box-shadow: 0 10px 34px -6px rgba(10, 186, 181, 0.6),
296
+ box-shadow:
297
+ 0 10px 34px -6px rgba(10, 186, 181, 0.6),
272
298
  0 4px 12px -4px rgba(10, 186, 181, 0.45);
273
299
  }
274
300
 
@@ -5,7 +5,7 @@
5
5
  <meta charset="UTF-8" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <meta name="color-scheme" content="light" />
8
- <title>Lambobo Studio</title>
8
+ <title>EffectNode Media</title>
9
9
  </head>
10
10
 
11
11
  <body>
@@ -3,19 +3,21 @@ import { create } from "zustand";
3
3
  const API_BASE = `http://localhost:${(window as any).PORT}`;
4
4
 
5
5
  /** Models that have a dedicated download endpoint in the backend. */
6
- export type AiModelId = "z-image" | "flux" | "qwen";
6
+ export type AiModelId = "z-image" | "flux" | "ltx" | "tts";
7
7
 
8
8
  /** Tools whose "install" step must run before their models can be downloaded. */
9
- export type AiToolId = "mlxgen" | "mlx-vlm";
9
+ export type AiToolId = "mlxgen" | "mlx-vlm" | "hf-cli";
10
10
 
11
11
  interface AiModelStore {
12
12
  // Tool installation status
13
13
  mlxgenInstalled: boolean | null;
14
14
  mlxVlmInstalled: boolean | null;
15
+ hfInstalled: boolean | null;
15
16
  // Model download status
16
17
  zImageDownloaded: boolean | null;
17
18
  fluxDownloaded: boolean | null;
18
- qwenDownloaded: boolean | null;
19
+ ltxDownloaded: boolean | null;
20
+ ttsDownloaded: boolean | null;
19
21
  // In-flight install/download (a model id or tool id)
20
22
  downloading: string | null;
21
23
  logs: string[];
@@ -65,40 +67,47 @@ async function readSSE(
65
67
  const DOWNLOAD_ENDPOINTS: Record<AiModelId, string> = {
66
68
  "z-image": "/api/mlxgen/download-z-model",
67
69
  flux: "/api/mlxgen/download-flux-model",
68
- qwen: "/api/mlxgen/download-model",
70
+ ltx: "/api/hf/download-ltx",
71
+ tts: "/api/hf/download-tts",
69
72
  };
70
73
 
71
74
  const TOOL_ENDPOINTS: Record<AiToolId, string> = {
72
75
  mlxgen: "/api/mlxgen/install",
73
76
  "mlx-vlm": "/api/agent/install",
77
+ "hf-cli": "/api/hf/install",
74
78
  };
75
79
 
76
80
  export const useAiModelStore = create<AiModelStore>((set, get) => ({
77
81
  mlxgenInstalled: null,
78
82
  mlxVlmInstalled: null,
83
+ hfInstalled: null,
79
84
  zImageDownloaded: null,
80
85
  fluxDownloaded: null,
81
- qwenDownloaded: null,
86
+ ltxDownloaded: null,
87
+ ttsDownloaded: null,
82
88
  downloading: null,
83
89
  logs: [],
84
90
  error: null,
85
91
 
86
92
  checkStatus: async () => {
87
93
  try {
88
- const [mlxgen, agent] = await Promise.all([
94
+ const [mlxgen, agent, hf] = await Promise.all([
89
95
  fetch(`${API_BASE}/api/mlxgen/status`).then((r) =>
90
96
  r.ok ? r.json() : null,
91
97
  ),
92
98
  fetch(`${API_BASE}/api/agent/status`).then((r) =>
93
99
  r.ok ? r.json() : null,
94
100
  ),
101
+ fetch(`${API_BASE}/api/hf/status`).then((r) => (r.ok ? r.json() : null)),
95
102
  ]);
96
103
  set({
97
104
  mlxgenInstalled: mlxgen ? Boolean(mlxgen.installed) : null,
98
- qwenDownloaded: mlxgen ? Boolean(mlxgen.modelDownloaded) : null,
99
105
  zImageDownloaded: mlxgen ? Boolean(mlxgen.zModelDownloaded) : null,
100
106
  fluxDownloaded: mlxgen ? Boolean(mlxgen.fluxModelDownloaded) : null,
101
107
  mlxVlmInstalled: agent ? Boolean(agent.installed) : null,
108
+ hfInstalled: hf ? Boolean(hf.installed) : null,
109
+ ltxDownloaded: hf ? Boolean(hf.ltxDownloaded) : null,
110
+ ttsDownloaded: hf ? Boolean(hf.ttsDownloaded) : null,
102
111
  });
103
112
  } catch {
104
113
  // Leave status unknown (null) if the checks fail.
@@ -18,7 +18,6 @@ export type GenerationTab =
18
18
  | "extend"
19
19
  | "agent"
20
20
  | "storyWriter"
21
- | "characters"
22
21
  | "extract"
23
22
  | "sceneVisual"
24
23
  | "textToImage"
@@ -143,6 +143,8 @@ interface MovieStudioStore {
143
143
  projectId: string | null;
144
144
  hydrated: boolean;
145
145
  generating: boolean;
146
+ generateStatus: string | null;
147
+ generateProgress: { current: number; total: number } | null;
146
148
  result: MovieStudioResult | null;
147
149
  error: string | null;
148
150
  rendering: boolean;
@@ -197,6 +199,8 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
197
199
  projectId: null,
198
200
  hydrated: false,
199
201
  generating: false,
202
+ generateStatus: null,
203
+ generateProgress: null,
200
204
  result: null,
201
205
  error: null,
202
206
  rendering: false,
@@ -250,6 +254,9 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
250
254
  assets: Array.isArray(stored.assets) ? stored.assets : [],
251
255
  videos: Array.isArray(stored.videos) ? stored.videos : [],
252
256
  sceneImages: Array.isArray(stored.sceneImages) ? stored.sceneImages : [],
257
+ renderedScenes: Array.isArray(stored.renderedScenes)
258
+ ? stored.renderedScenes
259
+ : [],
253
260
  });
254
261
  } catch {
255
262
  // Ignore — keep in-memory defaults.
@@ -465,14 +472,28 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
465
472
  if (task.status === "completed" && task.result) {
466
473
  if (!appliedCompleted.has(task.id)) {
467
474
  appliedCompleted.add(task.id);
468
- set({ result: task.result, generating: false });
475
+ set({
476
+ result: task.result,
477
+ generating: false,
478
+ generateStatus: null,
479
+ generateProgress: null,
480
+ });
469
481
  persistMovieStudioState();
470
482
  playDing3x();
471
483
  }
472
484
  } else if (err) {
473
- set({ generating: false, error: err });
485
+ set({
486
+ generating: false,
487
+ error: err,
488
+ generateStatus: null,
489
+ generateProgress: null,
490
+ });
474
491
  } else {
475
- set({ generating: isActive });
492
+ set({
493
+ generating: isActive,
494
+ generateStatus: task.status === "running" ? task.statusText : null,
495
+ generateProgress: task.progress,
496
+ });
476
497
  }
477
498
  break;
478
499
  }
@@ -708,6 +729,8 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
708
729
  set({
709
730
  idea: "",
710
731
  generating: false,
732
+ generateStatus: null,
733
+ generateProgress: null,
711
734
  result: null,
712
735
  error: null,
713
736
  rendering: false,
@@ -749,6 +772,7 @@ function persistMovieStudioState() {
749
772
  assets: s.assets,
750
773
  videos: s.videos,
751
774
  sceneImages: s.sceneImages,
775
+ renderedScenes: s.renderedScenes,
752
776
  }),
753
777
  }).catch(() => {});
754
778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectnode/media",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Start a full-stack media app: Vite + React + TypeScript frontend, Express backend with REST + WebSocket API",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "deploy": "npm run build; npm version minor; npm publish --access public",
21
21
  "build": "tsc && node -e \"const fs=require('node:fs');fs.cpSync('src/backend/movie-backend/agent/prompt','dist/backend/movie-backend/agent/prompt',{recursive:true})\"",
22
- "dev": "concurrently -k -n backend,frontend -c blue,green \"bun run dev:backend\" \"bun run dev:frontend\"",
22
+ "dev": "concurrently -k -n backend,frontend -c blue,green \"bun run dev:backend\" \"bun run dev:frontend\"; open http://localhsot:5177",
23
23
  "dev:backend": "nodemon",
24
24
  "dev:frontend": "vite",
25
25
  "start": "node bin/effectnode-media"