@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.
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
- package/dist/backend/movie-backend/agent/agent-backend.js +169 -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.js +107 -44
- package/dist/backend/movie-backend/render-media.js +28 -254
- package/frontend/index.html +6 -1
- package/frontend/public/lambobo.png +0 -0
- package/frontend/src/movie-app/MediaStudio.tsx +6 -2
- package/frontend/src/movie-app/SetupPage.tsx +98 -63
- package/frontend/src/movie-app/components/Aurora.tsx +13 -0
- package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +5 -8
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +417 -205
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +350 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -67
- package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
- package/frontend/src/movie-app/index.css +275 -8
- package/frontend/src/movie-app/index.html +1 -1
- package/frontend/src/movie-app/stores/aiModelStore.ts +172 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2 -3
- package/frontend/src/movie-app/stores/movieStudioStore.ts +72 -3
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
- 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({
|
|
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({
|
|
485
|
+
set({
|
|
486
|
+
generating: false,
|
|
487
|
+
error: err,
|
|
488
|
+
generateStatus: null,
|
|
489
|
+
generateProgress: null,
|
|
490
|
+
});
|
|
429
491
|
} else {
|
|
430
|
-
set({
|
|
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.
|
|
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"
|