@effectnode/media 0.3.0 → 0.5.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.js +4 -4
- package/dist/backend/movie-backend/agent/prompt/story-writer.txt +137 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/edit-image.js +1 -1
- package/dist/backend/movie-backend/agent/tools/get-time.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/grep-files.js +1 -1
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +1 -1
- package/dist/backend/movie-backend/agent/tools/index.d.ts +2 -2
- package/dist/backend/movie-backend/agent/tools/index.js +13 -13
- package/dist/backend/movie-backend/agent/tools/list-files.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/list-files.js +1 -1
- package/dist/backend/movie-backend/agent/tools/read-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/read-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/remove-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/rename-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/show-image.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/show-image.js +1 -1
- package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/stitch-videos.js +1 -1
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +1 -1
- package/dist/backend/movie-backend/agent/tools/update-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/update-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/write-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/write-file.js +1 -1
- package/dist/backend/movie-backend/core.js +4 -4
- package/dist/backend/movie-backend/generation-queue.js +102 -46
- package/dist/backend/movie-backend/render-media.js +1 -1
- package/frontend/index.html +8 -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 +297 -189
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +345 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -34
- package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
- package/frontend/src/movie-app/index.css +260 -19
- package/frontend/src/movie-app/stores/aiModelStore.ts +163 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2 -2
- package/frontend/src/movie-app/stores/movieStudioStore.ts +45 -0
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +0 -881
|
@@ -0,0 +1,163 @@
|
|
|
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" | "qwen";
|
|
7
|
+
|
|
8
|
+
/** Tools whose "install" step must run before their models can be downloaded. */
|
|
9
|
+
export type AiToolId = "mlxgen" | "mlx-vlm";
|
|
10
|
+
|
|
11
|
+
interface AiModelStore {
|
|
12
|
+
// Tool installation status
|
|
13
|
+
mlxgenInstalled: boolean | null;
|
|
14
|
+
mlxVlmInstalled: boolean | null;
|
|
15
|
+
// Model download status
|
|
16
|
+
zImageDownloaded: boolean | null;
|
|
17
|
+
fluxDownloaded: boolean | null;
|
|
18
|
+
qwenDownloaded: boolean | null;
|
|
19
|
+
// In-flight install/download (a model id or tool id)
|
|
20
|
+
downloading: string | null;
|
|
21
|
+
logs: string[];
|
|
22
|
+
error: string | null;
|
|
23
|
+
|
|
24
|
+
checkStatus: () => Promise<void>;
|
|
25
|
+
installTool: (tool: AiToolId) => Promise<void>;
|
|
26
|
+
downloadModel: (id: AiModelId) => Promise<void>;
|
|
27
|
+
clearLogs: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Read a server-sent-events response, invoking `onEvent` per parsed event. */
|
|
31
|
+
async function readSSE(
|
|
32
|
+
response: Response,
|
|
33
|
+
onEvent: (event: string, data: any) => void,
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const reader = response.body?.getReader();
|
|
36
|
+
if (!reader) return;
|
|
37
|
+
const decoder = new TextDecoder();
|
|
38
|
+
let buffer = "";
|
|
39
|
+
try {
|
|
40
|
+
while (true) {
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done) break;
|
|
43
|
+
buffer += decoder.decode(value, { stream: true });
|
|
44
|
+
const lines = buffer.split("\n");
|
|
45
|
+
buffer = lines.pop() || "";
|
|
46
|
+
let eventType = "message";
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (line.startsWith("event: ")) {
|
|
49
|
+
eventType = line.slice(7).trim();
|
|
50
|
+
} else if (line.startsWith("data: ")) {
|
|
51
|
+
try {
|
|
52
|
+
onEvent(eventType, JSON.parse(line.slice(6)));
|
|
53
|
+
} catch {
|
|
54
|
+
// skip malformed lines
|
|
55
|
+
}
|
|
56
|
+
eventType = "message";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
reader.releaseLock();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DOWNLOAD_ENDPOINTS: Record<AiModelId, string> = {
|
|
66
|
+
"z-image": "/api/mlxgen/download-z-model",
|
|
67
|
+
flux: "/api/mlxgen/download-flux-model",
|
|
68
|
+
qwen: "/api/mlxgen/download-model",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const TOOL_ENDPOINTS: Record<AiToolId, string> = {
|
|
72
|
+
mlxgen: "/api/mlxgen/install",
|
|
73
|
+
"mlx-vlm": "/api/agent/install",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const useAiModelStore = create<AiModelStore>((set, get) => ({
|
|
77
|
+
mlxgenInstalled: null,
|
|
78
|
+
mlxVlmInstalled: null,
|
|
79
|
+
zImageDownloaded: null,
|
|
80
|
+
fluxDownloaded: null,
|
|
81
|
+
qwenDownloaded: null,
|
|
82
|
+
downloading: null,
|
|
83
|
+
logs: [],
|
|
84
|
+
error: null,
|
|
85
|
+
|
|
86
|
+
checkStatus: async () => {
|
|
87
|
+
try {
|
|
88
|
+
const [mlxgen, agent] = await Promise.all([
|
|
89
|
+
fetch(`${API_BASE}/api/mlxgen/status`).then((r) =>
|
|
90
|
+
r.ok ? r.json() : null,
|
|
91
|
+
),
|
|
92
|
+
fetch(`${API_BASE}/api/agent/status`).then((r) =>
|
|
93
|
+
r.ok ? r.json() : null,
|
|
94
|
+
),
|
|
95
|
+
]);
|
|
96
|
+
set({
|
|
97
|
+
mlxgenInstalled: mlxgen ? Boolean(mlxgen.installed) : null,
|
|
98
|
+
qwenDownloaded: mlxgen ? Boolean(mlxgen.modelDownloaded) : null,
|
|
99
|
+
zImageDownloaded: mlxgen ? Boolean(mlxgen.zModelDownloaded) : null,
|
|
100
|
+
fluxDownloaded: mlxgen ? Boolean(mlxgen.fluxModelDownloaded) : null,
|
|
101
|
+
mlxVlmInstalled: agent ? Boolean(agent.installed) : null,
|
|
102
|
+
});
|
|
103
|
+
} catch {
|
|
104
|
+
// Leave status unknown (null) if the checks fail.
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
installTool: async (tool) => {
|
|
109
|
+
if (get().downloading) return;
|
|
110
|
+
set({ downloading: tool, logs: [], error: null });
|
|
111
|
+
try {
|
|
112
|
+
const res = await fetch(`${API_BASE}${TOOL_ENDPOINTS[tool]}`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
set({ downloading: null, error: await res.text() });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await readSSE(res, (event, data) => {
|
|
120
|
+
if (event === "log") {
|
|
121
|
+
set((s) => ({ logs: [...s.logs, data.text ?? ""] }));
|
|
122
|
+
} else if (event === "complete") {
|
|
123
|
+
set({ downloading: null });
|
|
124
|
+
} else if (event === "error") {
|
|
125
|
+
set({ downloading: null, error: data.error ?? "Install failed" });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
} catch (e) {
|
|
129
|
+
set({ downloading: null, error: String(e) });
|
|
130
|
+
} finally {
|
|
131
|
+
get().checkStatus();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
downloadModel: async (id) => {
|
|
136
|
+
if (get().downloading) return;
|
|
137
|
+
set({ downloading: id, logs: [], error: null });
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`${API_BASE}${DOWNLOAD_ENDPOINTS[id]}`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
set({ downloading: null, error: await res.text() });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await readSSE(res, (event, data) => {
|
|
147
|
+
if (event === "log") {
|
|
148
|
+
set((s) => ({ logs: [...s.logs, data.text ?? ""] }));
|
|
149
|
+
} else if (event === "complete") {
|
|
150
|
+
set({ downloading: null });
|
|
151
|
+
} else if (event === "error") {
|
|
152
|
+
set({ downloading: null, error: data.error ?? "Download failed" });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
} catch (e) {
|
|
156
|
+
set({ downloading: null, error: String(e) });
|
|
157
|
+
} finally {
|
|
158
|
+
get().checkStatus();
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
clearLogs: () => set({ logs: [], error: null }),
|
|
163
|
+
}));
|
|
@@ -22,11 +22,11 @@ export type GenerationTab =
|
|
|
22
22
|
| "extract"
|
|
23
23
|
| "sceneVisual"
|
|
24
24
|
| "textToImage"
|
|
25
|
-
| "referencesToVideo"
|
|
26
25
|
| "batchVideo"
|
|
27
26
|
| "batchImageToVideo"
|
|
28
27
|
| "batchVoice"
|
|
29
|
-
| "llmServer"
|
|
28
|
+
| "llmServer"
|
|
29
|
+
| "aiModels";
|
|
30
30
|
export type AspectRatio = "1:1" | "16:9" | "9:16" | "4:3" | "3:4";
|
|
31
31
|
export type Resolution =
|
|
32
32
|
| "320p"
|
|
@@ -183,6 +183,9 @@ interface MovieStudioStore {
|
|
|
183
183
|
regenerateVideo: (projectId: string, slug: string) => Promise<void>;
|
|
184
184
|
renderSceneImages: (projectId: string) => Promise<void>;
|
|
185
185
|
regenerateSceneImage: (projectId: string, slug: string) => Promise<void>;
|
|
186
|
+
updateCharacter: (slug: string, patch: Partial<MovieCharacter>) => void;
|
|
187
|
+
updatePlace: (slug: string, patch: Partial<MoviePlace>) => void;
|
|
188
|
+
updateScene: (slug: string, patch: Partial<MovieScene>) => void;
|
|
186
189
|
applyQueueTask: (task: QueueTask) => void;
|
|
187
190
|
primeAppliedQueue: (tasks: QueueTask[]) => void;
|
|
188
191
|
stop: () => void;
|
|
@@ -410,6 +413,48 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
410
413
|
}
|
|
411
414
|
},
|
|
412
415
|
|
|
416
|
+
updateCharacter: (slug, patch) => {
|
|
417
|
+
const result = get().result;
|
|
418
|
+
if (!result) return;
|
|
419
|
+
set({
|
|
420
|
+
result: {
|
|
421
|
+
...result,
|
|
422
|
+
characters: result.characters.map((c) =>
|
|
423
|
+
c.slug === slug ? { ...c, ...patch } : c,
|
|
424
|
+
),
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
persistMovieStudioState();
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
updatePlace: (slug, patch) => {
|
|
431
|
+
const result = get().result;
|
|
432
|
+
if (!result) return;
|
|
433
|
+
set({
|
|
434
|
+
result: {
|
|
435
|
+
...result,
|
|
436
|
+
places: result.places.map((p) =>
|
|
437
|
+
p.slug === slug ? { ...p, ...patch } : p,
|
|
438
|
+
),
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
persistMovieStudioState();
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
updateScene: (slug, patch) => {
|
|
445
|
+
const result = get().result;
|
|
446
|
+
if (!result) return;
|
|
447
|
+
set({
|
|
448
|
+
result: {
|
|
449
|
+
...result,
|
|
450
|
+
scenes: result.scenes.map((s) =>
|
|
451
|
+
s.slug === slug ? { ...s, ...patch } : s,
|
|
452
|
+
),
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
persistMovieStudioState();
|
|
456
|
+
},
|
|
457
|
+
|
|
413
458
|
// Reconcile the movie studio store with the latest queue task state.
|
|
414
459
|
applyQueueTask: (task) => {
|
|
415
460
|
const isActive = task.status === "pending" || task.status === "running";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effectnode/media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": "",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"deploy": "npm run build; npm version minor; npm publish --access public",
|
|
21
|
-
"build": "tsc",
|
|
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
22
|
"dev": "concurrently -k -n backend,frontend -c blue,green \"bun run dev:backend\" \"bun run dev:frontend\"",
|
|
23
23
|
"dev:backend": "nodemon",
|
|
24
24
|
"dev:frontend": "vite",
|