@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,172 @@
1
+ import { create } from "zustand";
2
+
3
+ const API_BASE = `http://localhost:${(window as any).PORT}`;
4
+
5
+ /** Models that have a dedicated download endpoint in the backend. */
6
+ export type AiModelId = "z-image" | "flux" | "ltx" | "tts";
7
+
8
+ /** Tools whose "install" step must run before their models can be downloaded. */
9
+ export type AiToolId = "mlxgen" | "mlx-vlm" | "hf-cli";
10
+
11
+ interface AiModelStore {
12
+ // Tool installation status
13
+ mlxgenInstalled: boolean | null;
14
+ mlxVlmInstalled: boolean | null;
15
+ hfInstalled: boolean | null;
16
+ // Model download status
17
+ zImageDownloaded: boolean | null;
18
+ fluxDownloaded: boolean | null;
19
+ ltxDownloaded: boolean | null;
20
+ ttsDownloaded: boolean | null;
21
+ // In-flight install/download (a model id or tool id)
22
+ downloading: string | null;
23
+ logs: string[];
24
+ error: string | null;
25
+
26
+ checkStatus: () => Promise<void>;
27
+ installTool: (tool: AiToolId) => Promise<void>;
28
+ downloadModel: (id: AiModelId) => Promise<void>;
29
+ clearLogs: () => void;
30
+ }
31
+
32
+ /** Read a server-sent-events response, invoking `onEvent` per parsed event. */
33
+ async function readSSE(
34
+ response: Response,
35
+ onEvent: (event: string, data: any) => void,
36
+ ): Promise<void> {
37
+ const reader = response.body?.getReader();
38
+ if (!reader) return;
39
+ const decoder = new TextDecoder();
40
+ let buffer = "";
41
+ try {
42
+ while (true) {
43
+ const { done, value } = await reader.read();
44
+ if (done) break;
45
+ buffer += decoder.decode(value, { stream: true });
46
+ const lines = buffer.split("\n");
47
+ buffer = lines.pop() || "";
48
+ let eventType = "message";
49
+ for (const line of lines) {
50
+ if (line.startsWith("event: ")) {
51
+ eventType = line.slice(7).trim();
52
+ } else if (line.startsWith("data: ")) {
53
+ try {
54
+ onEvent(eventType, JSON.parse(line.slice(6)));
55
+ } catch {
56
+ // skip malformed lines
57
+ }
58
+ eventType = "message";
59
+ }
60
+ }
61
+ }
62
+ } finally {
63
+ reader.releaseLock();
64
+ }
65
+ }
66
+
67
+ const DOWNLOAD_ENDPOINTS: Record<AiModelId, string> = {
68
+ "z-image": "/api/mlxgen/download-z-model",
69
+ flux: "/api/mlxgen/download-flux-model",
70
+ ltx: "/api/hf/download-ltx",
71
+ tts: "/api/hf/download-tts",
72
+ };
73
+
74
+ const TOOL_ENDPOINTS: Record<AiToolId, string> = {
75
+ mlxgen: "/api/mlxgen/install",
76
+ "mlx-vlm": "/api/agent/install",
77
+ "hf-cli": "/api/hf/install",
78
+ };
79
+
80
+ export const useAiModelStore = create<AiModelStore>((set, get) => ({
81
+ mlxgenInstalled: null,
82
+ mlxVlmInstalled: null,
83
+ hfInstalled: null,
84
+ zImageDownloaded: null,
85
+ fluxDownloaded: null,
86
+ ltxDownloaded: null,
87
+ ttsDownloaded: null,
88
+ downloading: null,
89
+ logs: [],
90
+ error: null,
91
+
92
+ checkStatus: async () => {
93
+ try {
94
+ const [mlxgen, agent, hf] = await Promise.all([
95
+ fetch(`${API_BASE}/api/mlxgen/status`).then((r) =>
96
+ r.ok ? r.json() : null,
97
+ ),
98
+ fetch(`${API_BASE}/api/agent/status`).then((r) =>
99
+ r.ok ? r.json() : null,
100
+ ),
101
+ fetch(`${API_BASE}/api/hf/status`).then((r) => (r.ok ? r.json() : null)),
102
+ ]);
103
+ set({
104
+ mlxgenInstalled: mlxgen ? Boolean(mlxgen.installed) : null,
105
+ zImageDownloaded: mlxgen ? Boolean(mlxgen.zModelDownloaded) : null,
106
+ fluxDownloaded: mlxgen ? Boolean(mlxgen.fluxModelDownloaded) : null,
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,
111
+ });
112
+ } catch {
113
+ // Leave status unknown (null) if the checks fail.
114
+ }
115
+ },
116
+
117
+ installTool: async (tool) => {
118
+ if (get().downloading) return;
119
+ set({ downloading: tool, logs: [], error: null });
120
+ try {
121
+ const res = await fetch(`${API_BASE}${TOOL_ENDPOINTS[tool]}`, {
122
+ method: "POST",
123
+ });
124
+ if (!res.ok) {
125
+ set({ downloading: null, error: await res.text() });
126
+ return;
127
+ }
128
+ await readSSE(res, (event, data) => {
129
+ if (event === "log") {
130
+ set((s) => ({ logs: [...s.logs, data.text ?? ""] }));
131
+ } else if (event === "complete") {
132
+ set({ downloading: null });
133
+ } else if (event === "error") {
134
+ set({ downloading: null, error: data.error ?? "Install failed" });
135
+ }
136
+ });
137
+ } catch (e) {
138
+ set({ downloading: null, error: String(e) });
139
+ } finally {
140
+ get().checkStatus();
141
+ }
142
+ },
143
+
144
+ downloadModel: async (id) => {
145
+ if (get().downloading) return;
146
+ set({ downloading: id, logs: [], error: null });
147
+ try {
148
+ const res = await fetch(`${API_BASE}${DOWNLOAD_ENDPOINTS[id]}`, {
149
+ method: "POST",
150
+ });
151
+ if (!res.ok) {
152
+ set({ downloading: null, error: await res.text() });
153
+ return;
154
+ }
155
+ await readSSE(res, (event, data) => {
156
+ if (event === "log") {
157
+ set((s) => ({ logs: [...s.logs, data.text ?? ""] }));
158
+ } else if (event === "complete") {
159
+ set({ downloading: null });
160
+ } else if (event === "error") {
161
+ set({ downloading: null, error: data.error ?? "Download failed" });
162
+ }
163
+ });
164
+ } catch (e) {
165
+ set({ downloading: null, error: String(e) });
166
+ } finally {
167
+ get().checkStatus();
168
+ }
169
+ },
170
+
171
+ clearLogs: () => set({ logs: [], error: null }),
172
+ }));
@@ -18,15 +18,14 @@ export type GenerationTab =
18
18
  | "extend"
19
19
  | "agent"
20
20
  | "storyWriter"
21
- | "characters"
22
21
  | "extract"
23
22
  | "sceneVisual"
24
23
  | "textToImage"
25
- | "referencesToVideo"
26
24
  | "batchVideo"
27
25
  | "batchImageToVideo"
28
26
  | "batchVoice"
29
- | "llmServer";
27
+ | "llmServer"
28
+ | "aiModels";
30
29
  export type AspectRatio = "1:1" | "16:9" | "9:16" | "4:3" | "3:4";
31
30
  export type Resolution =
32
31
  | "320p"
@@ -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;
@@ -183,6 +185,9 @@ interface MovieStudioStore {
183
185
  regenerateVideo: (projectId: string, slug: string) => Promise<void>;
184
186
  renderSceneImages: (projectId: string) => Promise<void>;
185
187
  regenerateSceneImage: (projectId: string, slug: string) => Promise<void>;
188
+ updateCharacter: (slug: string, patch: Partial<MovieCharacter>) => void;
189
+ updatePlace: (slug: string, patch: Partial<MoviePlace>) => void;
190
+ updateScene: (slug: string, patch: Partial<MovieScene>) => void;
186
191
  applyQueueTask: (task: QueueTask) => void;
187
192
  primeAppliedQueue: (tasks: QueueTask[]) => void;
188
193
  stop: () => void;
@@ -194,6 +199,8 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
194
199
  projectId: null,
195
200
  hydrated: false,
196
201
  generating: false,
202
+ generateStatus: null,
203
+ generateProgress: null,
197
204
  result: null,
198
205
  error: null,
199
206
  rendering: false,
@@ -247,6 +254,9 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
247
254
  assets: Array.isArray(stored.assets) ? stored.assets : [],
248
255
  videos: Array.isArray(stored.videos) ? stored.videos : [],
249
256
  sceneImages: Array.isArray(stored.sceneImages) ? stored.sceneImages : [],
257
+ renderedScenes: Array.isArray(stored.renderedScenes)
258
+ ? stored.renderedScenes
259
+ : [],
250
260
  });
251
261
  } catch {
252
262
  // Ignore — keep in-memory defaults.
@@ -410,6 +420,48 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
410
420
  }
411
421
  },
412
422
 
423
+ updateCharacter: (slug, patch) => {
424
+ const result = get().result;
425
+ if (!result) return;
426
+ set({
427
+ result: {
428
+ ...result,
429
+ characters: result.characters.map((c) =>
430
+ c.slug === slug ? { ...c, ...patch } : c,
431
+ ),
432
+ },
433
+ });
434
+ persistMovieStudioState();
435
+ },
436
+
437
+ updatePlace: (slug, patch) => {
438
+ const result = get().result;
439
+ if (!result) return;
440
+ set({
441
+ result: {
442
+ ...result,
443
+ places: result.places.map((p) =>
444
+ p.slug === slug ? { ...p, ...patch } : p,
445
+ ),
446
+ },
447
+ });
448
+ persistMovieStudioState();
449
+ },
450
+
451
+ updateScene: (slug, patch) => {
452
+ const result = get().result;
453
+ if (!result) return;
454
+ set({
455
+ result: {
456
+ ...result,
457
+ scenes: result.scenes.map((s) =>
458
+ s.slug === slug ? { ...s, ...patch } : s,
459
+ ),
460
+ },
461
+ });
462
+ persistMovieStudioState();
463
+ },
464
+
413
465
  // Reconcile the movie studio store with the latest queue task state.
414
466
  applyQueueTask: (task) => {
415
467
  const isActive = task.status === "pending" || task.status === "running";
@@ -420,14 +472,28 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
420
472
  if (task.status === "completed" && task.result) {
421
473
  if (!appliedCompleted.has(task.id)) {
422
474
  appliedCompleted.add(task.id);
423
- set({ result: task.result, generating: false });
475
+ set({
476
+ result: task.result,
477
+ generating: false,
478
+ generateStatus: null,
479
+ generateProgress: null,
480
+ });
424
481
  persistMovieStudioState();
425
482
  playDing3x();
426
483
  }
427
484
  } else if (err) {
428
- set({ generating: false, error: err });
485
+ set({
486
+ generating: false,
487
+ error: err,
488
+ generateStatus: null,
489
+ generateProgress: null,
490
+ });
429
491
  } else {
430
- set({ generating: isActive });
492
+ set({
493
+ generating: isActive,
494
+ generateStatus: task.status === "running" ? task.statusText : null,
495
+ generateProgress: task.progress,
496
+ });
431
497
  }
432
498
  break;
433
499
  }
@@ -663,6 +729,8 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
663
729
  set({
664
730
  idea: "",
665
731
  generating: false,
732
+ generateStatus: null,
733
+ generateProgress: null,
666
734
  result: null,
667
735
  error: null,
668
736
  rendering: false,
@@ -704,6 +772,7 @@ function persistMovieStudioState() {
704
772
  assets: s.assets,
705
773
  videos: s.videos,
706
774
  sceneImages: s.sceneImages,
775
+ renderedScenes: s.renderedScenes,
707
776
  }),
708
777
  }).catch(() => {});
709
778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectnode/media",
3
- "version": "0.4.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"