@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.
Files changed (24) hide show
  1. package/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
  2. package/dist/backend/movie-backend/agent/agent-backend.js +169 -83
  3. package/dist/backend/movie-backend/agent/tools/index.js +0 -2
  4. package/dist/backend/movie-backend/core.js +1 -1
  5. package/dist/backend/movie-backend/generation-queue.js +107 -44
  6. package/dist/backend/movie-backend/render-media.js +28 -254
  7. package/frontend/index.html +6 -1
  8. package/frontend/public/lambobo.png +0 -0
  9. package/frontend/src/movie-app/MediaStudio.tsx +6 -2
  10. package/frontend/src/movie-app/SetupPage.tsx +98 -63
  11. package/frontend/src/movie-app/components/Aurora.tsx +13 -0
  12. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +5 -8
  13. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +417 -205
  14. package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +350 -0
  15. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -67
  16. package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
  17. package/frontend/src/movie-app/index.css +275 -8
  18. package/frontend/src/movie-app/index.html +1 -1
  19. package/frontend/src/movie-app/stores/aiModelStore.ts +172 -0
  20. package/frontend/src/movie-app/stores/generationStore.ts +2 -3
  21. package/frontend/src/movie-app/stores/movieStudioStore.ts +72 -3
  22. package/package.json +2 -2
  23. package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
  24. package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +0 -881
@@ -0,0 +1,350 @@
1
+ import { useEffect, type ReactNode } from "react";
2
+ import { useAiModelStore, type AiModelId } from "../../stores/aiModelStore";
3
+
4
+ const DownloadIcon = (
5
+ <svg
6
+ width="14"
7
+ height="14"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
16
+ <polyline points="7 10 12 15 17 10" />
17
+ <line x1="12" y1="15" x2="12" y2="3" />
18
+ </svg>
19
+ );
20
+
21
+ const InstallIcon = (
22
+ <svg
23
+ width="14"
24
+ height="14"
25
+ viewBox="0 0 24 24"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ strokeWidth="2"
29
+ strokeLinecap="round"
30
+ strokeLinejoin="round"
31
+ >
32
+ <polyline points="16 18 22 12 16 6" />
33
+ <polyline points="8 6 2 12 8 18" />
34
+ </svg>
35
+ );
36
+
37
+ const CheckIcon = (
38
+ <svg
39
+ width="14"
40
+ height="14"
41
+ viewBox="0 0 24 24"
42
+ fill="none"
43
+ stroke="currentColor"
44
+ strokeWidth="2"
45
+ strokeLinecap="round"
46
+ strokeLinejoin="round"
47
+ >
48
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
49
+ <polyline points="22 4 12 14.01 9 11.01" />
50
+ </svg>
51
+ );
52
+
53
+ const AlertIcon = (
54
+ <svg
55
+ width="14"
56
+ height="14"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ strokeWidth="2"
61
+ strokeLinecap="round"
62
+ strokeLinejoin="round"
63
+ >
64
+ <circle cx="12" cy="12" r="10" />
65
+ <line x1="12" y1="8" x2="12" y2="12" />
66
+ <line x1="12" y1="16" x2="12.01" y2="16" />
67
+ </svg>
68
+ );
69
+
70
+ const SpinnerIcon = (
71
+ <svg
72
+ className="animate-spin"
73
+ width="14"
74
+ height="14"
75
+ viewBox="0 0 24 24"
76
+ fill="none"
77
+ stroke="currentColor"
78
+ strokeWidth="2"
79
+ >
80
+ <circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
81
+ <path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
82
+ </svg>
83
+ );
84
+
85
+ const RefreshIcon = (
86
+ <svg
87
+ width="14"
88
+ height="14"
89
+ viewBox="0 0 24 24"
90
+ fill="none"
91
+ stroke="currentColor"
92
+ strokeWidth="2"
93
+ strokeLinecap="round"
94
+ strokeLinejoin="round"
95
+ >
96
+ <polyline points="23 4 23 10 17 10" />
97
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
98
+ </svg>
99
+ );
100
+
101
+ const ChipIcon = (
102
+ <svg
103
+ width="18"
104
+ height="18"
105
+ viewBox="0 0 24 24"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ strokeWidth="2"
109
+ strokeLinecap="round"
110
+ strokeLinejoin="round"
111
+ >
112
+ <rect x="4" y="4" width="16" height="16" rx="2" ry="2" />
113
+ <rect x="9" y="9" width="6" height="6" />
114
+ <line x1="9" y1="1" x2="9" y2="4" />
115
+ <line x1="15" y1="1" x2="15" y2="4" />
116
+ <line x1="9" y1="20" x2="9" y2="23" />
117
+ <line x1="15" y1="20" x2="15" y2="23" />
118
+ <line x1="20" y1="9" x2="23" y2="9" />
119
+ <line x1="20" y1="14" x2="23" y2="14" />
120
+ <line x1="1" y1="9" x2="4" y2="9" />
121
+ <line x1="1" y1="14" x2="4" y2="14" />
122
+ </svg>
123
+ );
124
+
125
+ function StatusBadge({
126
+ value,
127
+ okText = "Downloaded",
128
+ missingText = "Not downloaded",
129
+ }: {
130
+ value: boolean | null;
131
+ okText?: string;
132
+ missingText?: string;
133
+ }) {
134
+ if (value === null) {
135
+ return (
136
+ <span className="inline-flex items-center gap-1.5 text-xs text-ink-500">
137
+ {SpinnerIcon}
138
+ Checking…
139
+ </span>
140
+ );
141
+ }
142
+ if (value) {
143
+ return (
144
+ <span className="inline-flex items-center gap-1.5 text-xs font-medium text-emerald-600">
145
+ {CheckIcon}
146
+ {okText}
147
+ </span>
148
+ );
149
+ }
150
+ return (
151
+ <span className="inline-flex items-center gap-1.5 text-xs font-medium text-amber-600">
152
+ {AlertIcon}
153
+ {missingText}
154
+ </span>
155
+ );
156
+ }
157
+
158
+ function ModelRow({
159
+ name,
160
+ desc,
161
+ children,
162
+ }: {
163
+ name: string;
164
+ desc: string;
165
+ children?: ReactNode;
166
+ }) {
167
+ return (
168
+ <div className="flex items-center gap-3 rounded-2xl border border-ink-200 bg-white px-4 py-3">
169
+ <div className="min-w-0 flex-1">
170
+ <p className="truncate font-mono text-sm text-ink-900">{name}</p>
171
+ <p className="mt-0.5 text-xs text-ink-500">{desc}</p>
172
+ </div>
173
+ {children}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ const DOWNLOAD_MODELS: {
179
+ id: AiModelId;
180
+ name: string;
181
+ desc: string;
182
+ }[] = [
183
+ {
184
+ id: "z-image",
185
+ name: "AbstractFramework/z-image-turbo-8bit",
186
+ desc: "Text-to-image",
187
+ },
188
+ {
189
+ id: "flux",
190
+ name: "AbstractFramework/flux.2-klein-4b-8bit",
191
+ desc: "Character, place & scene images",
192
+ },
193
+ {
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",
202
+ },
203
+ ];
204
+
205
+ export default function SetupAiModelTab() {
206
+ const store = useAiModelStore();
207
+
208
+ useEffect(() => {
209
+ store.checkStatus();
210
+ const interval = setInterval(() => store.checkStatus(), 3000);
211
+ return () => clearInterval(interval);
212
+ // eslint-disable-next-line react-hooks/exhaustive-deps
213
+ }, []);
214
+
215
+ const busy = store.downloading !== null;
216
+
217
+ return (
218
+ <div className="flex flex-col gap-6">
219
+ {/* Header */}
220
+ <div className="flex items-center gap-2">
221
+ <span className="text-tiffany-600">{ChipIcon}</span>
222
+ <h2 className="text-base font-semibold text-ink-900">AI Models</h2>
223
+ <button
224
+ onClick={() => store.checkStatus()}
225
+ className="ml-auto flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900 transition-colors"
226
+ >
227
+ {RefreshIcon}
228
+ Refresh
229
+ </button>
230
+ </div>
231
+
232
+ {/* Engines */}
233
+ <section>
234
+ <h3 className="mb-2 text-sm font-semibold text-ink-900">Engines</h3>
235
+ <div className="space-y-2">
236
+ <ModelRow name="mlx-gen" desc="Image generation engine (z-image · flux)">
237
+ <StatusBadge
238
+ value={store.mlxgenInstalled}
239
+ okText="Installed"
240
+ missingText="Not installed"
241
+ />
242
+ <button
243
+ onClick={() => store.installTool("mlxgen")}
244
+ disabled={busy}
245
+ 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"
246
+ >
247
+ {store.downloading === "mlxgen" ? SpinnerIcon : InstallIcon}
248
+ {store.downloading === "mlxgen" ? "Installing…" : "Install"}
249
+ </button>
250
+ </ModelRow>
251
+
252
+ <ModelRow name="mlx-vlm" desc="LLM server (gemma) for story planning">
253
+ <StatusBadge
254
+ value={store.mlxVlmInstalled}
255
+ okText="Installed"
256
+ missingText="Not installed"
257
+ />
258
+ <button
259
+ onClick={() => store.installTool("mlx-vlm")}
260
+ disabled={busy}
261
+ 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"
262
+ >
263
+ {store.downloading === "mlx-vlm" ? SpinnerIcon : InstallIcon}
264
+ {store.downloading === "mlx-vlm" ? "Installing…" : "Install"}
265
+ </button>
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>
283
+ </div>
284
+ </section>
285
+
286
+ {/* Models */}
287
+ <section>
288
+ <h3 className="mb-2 text-sm font-semibold text-ink-900">Models</h3>
289
+ <div className="space-y-2">
290
+ <ModelRow
291
+ name="mlx-community/gemma-4-e2b-it-4bit"
292
+ desc="Story planning & dialogue (LLM)"
293
+ >
294
+ <StatusBadge
295
+ value={store.mlxVlmInstalled}
296
+ okText="Ready"
297
+ missingText="Not set up"
298
+ />
299
+ <span className="text-xs italic text-ink-400">
300
+ Downloads on first server start
301
+ </span>
302
+ </ModelRow>
303
+
304
+ {DOWNLOAD_MODELS.map((m) => {
305
+ const downloaded =
306
+ m.id === "z-image"
307
+ ? store.zImageDownloaded
308
+ : m.id === "flux"
309
+ ? store.fluxDownloaded
310
+ : m.id === "ltx"
311
+ ? store.ltxDownloaded
312
+ : store.ttsDownloaded;
313
+ return (
314
+ <ModelRow key={m.id} name={m.name} desc={m.desc}>
315
+ <StatusBadge value={downloaded} />
316
+ <button
317
+ onClick={() => store.downloadModel(m.id)}
318
+ disabled={busy}
319
+ className="flex items-center gap-1.5 whitespace-nowrap rounded-xl bg-tiffany-500 px-3 py-1.5 text-xs font-medium text-ink-950 transition-colors hover:bg-tiffany-600 disabled:opacity-50"
320
+ >
321
+ {store.downloading === m.id ? SpinnerIcon : DownloadIcon}
322
+ {store.downloading === m.id ? "Downloading…" : "Download"}
323
+ </button>
324
+ </ModelRow>
325
+ );
326
+ })}
327
+ </div>
328
+ </section>
329
+
330
+ {/* Error */}
331
+ {store.error && (
332
+ <div className="rounded-2xl border border-rose-200 bg-rose-50/70 px-4 py-3 text-xs text-rose-600">
333
+ {store.error}
334
+ </div>
335
+ )}
336
+
337
+ {/* Download logs */}
338
+ {store.logs.length > 0 && (
339
+ <div className="rounded-2xl border border-ink-200 bg-ink-50 p-4">
340
+ <p className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-ink-500">
341
+ Output
342
+ </p>
343
+ <pre className="max-h-48 overflow-y-auto whitespace-pre-wrap font-mono text-xs leading-relaxed text-ink-700">
344
+ {store.logs.join("")}
345
+ </pre>
346
+ </div>
347
+ )}
348
+ </div>
349
+ );
350
+ }
@@ -8,15 +8,14 @@ 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";
15
- import ReferencesToVideoTab from "./EditorTabs/ReferencesToVideoTab";
16
14
  import BatchVideoTab from "./EditorTabs/BatchVideoTab";
17
15
  import BatchImageToVideoTab from "./EditorTabs/BatchImageToVideoTab";
18
16
  import BatchVoiceVideoTab from "./EditorTabs/BatchVoiceVideoTab";
19
17
  import LlmServerTab from "./EditorTabs/LlmServerTab";
18
+ import SetupAiModelTab from "./EditorTabs/SetupAiModelTab";
20
19
 
21
20
  export default function ProjectEditorPage() {
22
21
  const { id } = useParams<{ id: string }>();
@@ -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"
@@ -237,22 +220,6 @@ export default function ProjectEditorPage() {
237
220
  </svg>
238
221
  );
239
222
 
240
- const ReferencesToVideoIcon = (
241
- <svg
242
- width="18"
243
- height="18"
244
- viewBox="0 0 24 24"
245
- fill="none"
246
- stroke="currentColor"
247
- strokeWidth="2"
248
- strokeLinecap="round"
249
- strokeLinejoin="round"
250
- >
251
- <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
252
- <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
253
- </svg>
254
- );
255
-
256
223
  const BatchVideoIcon = (
257
224
  <svg
258
225
  width="18"
@@ -335,6 +302,30 @@ export default function ProjectEditorPage() {
335
302
  />
336
303
  );
337
304
 
305
+ const AiModelIcon = (
306
+ <svg
307
+ width="18"
308
+ height="18"
309
+ viewBox="0 0 24 24"
310
+ fill="none"
311
+ stroke="currentColor"
312
+ strokeWidth="2"
313
+ strokeLinecap="round"
314
+ strokeLinejoin="round"
315
+ >
316
+ <rect x="4" y="4" width="16" height="16" rx="2" ry="2" />
317
+ <rect x="9" y="9" width="6" height="6" />
318
+ <line x1="9" y1="1" x2="9" y2="4" />
319
+ <line x1="15" y1="1" x2="15" y2="4" />
320
+ <line x1="9" y1="20" x2="9" y2="23" />
321
+ <line x1="15" y1="20" x2="15" y2="23" />
322
+ <line x1="20" y1="9" x2="23" y2="9" />
323
+ <line x1="20" y1="14" x2="23" y2="14" />
324
+ <line x1="1" y1="9" x2="4" y2="9" />
325
+ <line x1="1" y1="14" x2="4" y2="14" />
326
+ </svg>
327
+ );
328
+
338
329
  // ========== Loading / Not Found ==========
339
330
 
340
331
  if (!project) {
@@ -417,7 +408,7 @@ export default function ProjectEditorPage() {
417
408
  }`}
418
409
  >
419
410
  {VideoIcon}
420
- Generate Avatar ideo
411
+ Generate Avatar Video
421
412
  </button>
422
413
  <button
423
414
  onClick={() => store.setActiveTab("extend")}
@@ -463,17 +454,6 @@ export default function ProjectEditorPage() {
463
454
  {StoryWriterIcon}
464
455
  Story Writer
465
456
  </button>
466
- <button
467
- onClick={() => store.setActiveTab("characters")}
468
- className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
469
- store.activeTab === "characters"
470
- ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
471
- : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
472
- }`}
473
- >
474
- {CharacterIcon}
475
- Characters
476
- </button>
477
457
  <button
478
458
  onClick={() => store.setActiveTab("extract")}
479
459
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
@@ -507,17 +487,6 @@ export default function ProjectEditorPage() {
507
487
  {TextToImageIcon}
508
488
  Text-to-Image
509
489
  </button>
510
- <button
511
- onClick={() => store.setActiveTab("referencesToVideo")}
512
- className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
513
- store.activeTab === "referencesToVideo"
514
- ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
515
- : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
516
- }`}
517
- >
518
- {ReferencesToVideoIcon}
519
- References to Video
520
- </button>
521
490
  <button
522
491
  onClick={() => store.setActiveTab("batchVideo")}
523
492
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
@@ -563,6 +532,17 @@ export default function ProjectEditorPage() {
563
532
  LLM Server
564
533
  <ServerStatusLight />
565
534
  </button>
535
+ <button
536
+ onClick={() => store.setActiveTab("aiModels")}
537
+ className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
538
+ store.activeTab === "aiModels"
539
+ ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
540
+ : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
541
+ }`}
542
+ >
543
+ {AiModelIcon}
544
+ Setup AI Models
545
+ </button>
566
546
  </div>
567
547
 
568
548
  {/* ========== MOVIE STUDIO PANEL ========== */}
@@ -589,11 +569,6 @@ export default function ProjectEditorPage() {
589
569
  <StoryWriterTab projectId={id!} />
590
570
  )}
591
571
 
592
- {/* ========== CHARACTERS PANEL ========== */}
593
- {store.activeTab === "characters" && (
594
- <CharactersTab projectId={id!} />
595
- )}
596
-
597
572
  {/* ========== EXTRACT IMAGE PANEL ========== */}
598
573
  {store.activeTab === "extract" && <ExtractImageTab projectId={id!} />}
599
574
 
@@ -607,11 +582,6 @@ export default function ProjectEditorPage() {
607
582
  <TextToImageTab projectId={id!} />
608
583
  )}
609
584
 
610
- {/* ========== REFERENCES TO VIDEO PANEL ========== */}
611
- {store.activeTab === "referencesToVideo" && (
612
- <ReferencesToVideoTab projectId={id!} />
613
- )}
614
-
615
585
  {/* ========== BATCH VIDEO PANEL ========== */}
616
586
  {store.activeTab === "batchVideo" && (
617
587
  <BatchVideoTab projectId={id!} />
@@ -629,6 +599,9 @@ export default function ProjectEditorPage() {
629
599
 
630
600
  {/* ========== LLM SERVER PANEL ========== */}
631
601
  {store.activeTab === "llmServer" && <LlmServerTab />}
602
+
603
+ {/* ========== SETUP AI MODELS PANEL ========== */}
604
+ {store.activeTab === "aiModels" && <SetupAiModelTab />}
632
605
  </div>
633
606
  </div>
634
607
  </div>