@effectnode/media 0.9.0 → 0.11.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/prompt/ltx.txt +20 -0
- package/dist/backend/movie-backend/agent/prompt/script.md +111 -1
- package/dist/backend/movie-backend/core.js +41 -0
- package/dist/backend/movie-backend/generation-queue.d.ts +1 -1
- package/dist/backend/movie-backend/generation-queue.js +75 -8
- package/dist/backend/movie-backend/render-media.d.ts +79 -2
- package/dist/backend/movie-backend/render-media.js +694 -418
- package/frontend/src/movie-app/components/EditorTabs/AdvancedVoiceCloneTab.tsx +366 -0
- package/frontend/src/movie-app/components/EditorTabs/AudioToVideoTab.tsx +406 -0
- package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +47 -0
- package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +155 -1
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +33 -13
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +26 -5
- package/frontend/src/movie-app/components/EditorTabs/UpscaleTab.tsx +343 -0
- package/frontend/src/movie-app/components/EditorTabs/VoiceCloneTab.tsx +365 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +130 -125
- package/frontend/src/movie-app/stores/advancedVoiceCloneStore.ts +205 -0
- package/frontend/src/movie-app/stores/aiModelStore.ts +25 -2
- package/frontend/src/movie-app/stores/audioToVideoStore.ts +274 -0
- package/frontend/src/movie-app/stores/generationStore.ts +156 -255
- package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
- package/frontend/src/movie-app/stores/queueStore.ts +47 -1
- package/frontend/src/movie-app/stores/upscaleStore.ts +118 -0
- package/frontend/src/movie-app/stores/voiceCloneStore.ts +227 -0
- package/package.json +1 -1
- package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +0 -913
- package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +0 -305
- package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +0 -249
- package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +0 -267
- package/frontend/src/movie-app/lib/batchVoiceStorage.ts +0 -75
- package/frontend/src/movie-app/stores/batchVoiceStore.ts +0 -990
- package/frontend/src/movie-app/stores/sceneVisualStore.ts +0 -251
|
@@ -3,7 +3,15 @@ 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 =
|
|
6
|
+
export type AiModelId =
|
|
7
|
+
| "z-image"
|
|
8
|
+
| "flux"
|
|
9
|
+
| "seedvr2"
|
|
10
|
+
| "ltx"
|
|
11
|
+
| "ltx-base"
|
|
12
|
+
| "tts"
|
|
13
|
+
| "gemma"
|
|
14
|
+
| "dots-tts";
|
|
7
15
|
|
|
8
16
|
/** Tools whose "install" step must run before their models can be downloaded. */
|
|
9
17
|
export type AiToolId = "mlxgen" | "mlx-vlm" | "hf-cli";
|
|
@@ -16,9 +24,12 @@ interface AiModelStore {
|
|
|
16
24
|
// Model download status
|
|
17
25
|
zImageDownloaded: boolean | null;
|
|
18
26
|
fluxDownloaded: boolean | null;
|
|
27
|
+
seedvr2Downloaded: boolean | null;
|
|
19
28
|
ltxDownloaded: boolean | null;
|
|
29
|
+
ltxBaseDownloaded: boolean | null;
|
|
20
30
|
ttsDownloaded: boolean | null;
|
|
21
31
|
gemmaDownloaded: boolean | null;
|
|
32
|
+
dotsTtsDownloaded: boolean | null;
|
|
22
33
|
// In-flight install/download (a model id or tool id)
|
|
23
34
|
downloading: string | null;
|
|
24
35
|
logs: string[];
|
|
@@ -68,9 +79,12 @@ async function readSSE(
|
|
|
68
79
|
const DOWNLOAD_ENDPOINTS: Record<AiModelId, string> = {
|
|
69
80
|
"z-image": "/api/mlxgen/download-z-model",
|
|
70
81
|
flux: "/api/mlxgen/download-flux-model",
|
|
82
|
+
seedvr2: "/api/mlxgen/download-seedvr2-model",
|
|
71
83
|
ltx: "/api/hf/download-ltx",
|
|
84
|
+
"ltx-base": "/api/hf/download-ltx-base",
|
|
72
85
|
tts: "/api/hf/download-tts",
|
|
73
86
|
gemma: "/api/hf/download-mlx-vlm",
|
|
87
|
+
"dots-tts": "/api/dots-tts/download-model",
|
|
74
88
|
};
|
|
75
89
|
|
|
76
90
|
const TOOL_ENDPOINTS: Record<AiToolId, string> = {
|
|
@@ -85,16 +99,19 @@ export const useAiModelStore = create<AiModelStore>((set, get) => ({
|
|
|
85
99
|
hfInstalled: null,
|
|
86
100
|
zImageDownloaded: null,
|
|
87
101
|
fluxDownloaded: null,
|
|
102
|
+
seedvr2Downloaded: null,
|
|
88
103
|
ltxDownloaded: null,
|
|
104
|
+
ltxBaseDownloaded: null,
|
|
89
105
|
ttsDownloaded: null,
|
|
90
106
|
gemmaDownloaded: null,
|
|
107
|
+
dotsTtsDownloaded: null,
|
|
91
108
|
downloading: null,
|
|
92
109
|
logs: [],
|
|
93
110
|
error: null,
|
|
94
111
|
|
|
95
112
|
checkStatus: async () => {
|
|
96
113
|
try {
|
|
97
|
-
const [mlxgen, agent, hf] = await Promise.all([
|
|
114
|
+
const [mlxgen, agent, hf, dotsTts] = await Promise.all([
|
|
98
115
|
fetch(`${API_BASE}/api/mlxgen/status`).then((r) =>
|
|
99
116
|
r.ok ? r.json() : null,
|
|
100
117
|
),
|
|
@@ -102,16 +119,22 @@ export const useAiModelStore = create<AiModelStore>((set, get) => ({
|
|
|
102
119
|
r.ok ? r.json() : null,
|
|
103
120
|
),
|
|
104
121
|
fetch(`${API_BASE}/api/hf/status`).then((r) => (r.ok ? r.json() : null)),
|
|
122
|
+
fetch(`${API_BASE}/api/dots-tts/status`).then((r) =>
|
|
123
|
+
r.ok ? r.json() : null,
|
|
124
|
+
),
|
|
105
125
|
]);
|
|
106
126
|
set({
|
|
107
127
|
mlxgenInstalled: mlxgen ? Boolean(mlxgen.installed) : null,
|
|
108
128
|
zImageDownloaded: mlxgen ? Boolean(mlxgen.zModelDownloaded) : null,
|
|
109
129
|
fluxDownloaded: mlxgen ? Boolean(mlxgen.fluxModelDownloaded) : null,
|
|
130
|
+
seedvr2Downloaded: mlxgen ? Boolean(mlxgen.seedvr2Downloaded) : null,
|
|
110
131
|
mlxVlmInstalled: agent ? Boolean(agent.installed) : null,
|
|
111
132
|
hfInstalled: hf ? Boolean(hf.installed) : null,
|
|
112
133
|
ltxDownloaded: hf ? Boolean(hf.ltxDownloaded) : null,
|
|
134
|
+
ltxBaseDownloaded: hf ? Boolean(hf.ltxBaseDownloaded) : null,
|
|
113
135
|
ttsDownloaded: hf ? Boolean(hf.ttsDownloaded) : null,
|
|
114
136
|
gemmaDownloaded: hf ? Boolean(hf.mlxVlmDownloaded) : null,
|
|
137
|
+
dotsTtsDownloaded: dotsTts ? Boolean(dotsTts.downloaded) : null,
|
|
115
138
|
});
|
|
116
139
|
} catch {
|
|
117
140
|
// Leave status unknown (null) if the checks fail.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { QueueTask } from "./queueStore";
|
|
3
|
+
|
|
4
|
+
const API_BASE = `http://localhost:${(window as any).PORT}`;
|
|
5
|
+
|
|
6
|
+
export interface AssetFile {
|
|
7
|
+
filename: string;
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolveUrl(url: string): string {
|
|
12
|
+
return url.startsWith("http") ? url : `${API_BASE}${url}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AudioToVideoStore {
|
|
16
|
+
image: AssetFile | null;
|
|
17
|
+
audio: AssetFile | null;
|
|
18
|
+
images: AssetFile[];
|
|
19
|
+
audios: AssetFile[];
|
|
20
|
+
imagesLoading: boolean;
|
|
21
|
+
audiosLoading: boolean;
|
|
22
|
+
uploadingImage: boolean;
|
|
23
|
+
uploadingAudio: boolean;
|
|
24
|
+
steps: number;
|
|
25
|
+
duration: number;
|
|
26
|
+
frames: number;
|
|
27
|
+
prompt: string;
|
|
28
|
+
generating: boolean;
|
|
29
|
+
result: string | null;
|
|
30
|
+
error: string | null;
|
|
31
|
+
|
|
32
|
+
setImage: (a: AssetFile | null) => void;
|
|
33
|
+
setAudio: (a: AssetFile | null) => void;
|
|
34
|
+
setSteps: (n: number) => void;
|
|
35
|
+
setDuration: (d: number) => void;
|
|
36
|
+
setFrames: (n: number) => void;
|
|
37
|
+
setPrompt: (p: string) => void;
|
|
38
|
+
clearResult: () => void;
|
|
39
|
+
fetchImages: (projectId: string) => Promise<void>;
|
|
40
|
+
fetchAudios: (projectId: string) => Promise<void>;
|
|
41
|
+
uploadImage: (
|
|
42
|
+
projectId: string,
|
|
43
|
+
base64: string,
|
|
44
|
+
filename: string,
|
|
45
|
+
) => Promise<void>;
|
|
46
|
+
uploadAudio: (
|
|
47
|
+
projectId: string,
|
|
48
|
+
base64: string,
|
|
49
|
+
filename: string,
|
|
50
|
+
) => Promise<void>;
|
|
51
|
+
generate: (projectId: string) => Promise<void>;
|
|
52
|
+
applyQueueTask: (task: QueueTask) => void;
|
|
53
|
+
reset: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The queue task id enqueued by the current generate action, if any. */
|
|
57
|
+
let audioToVideoActiveTaskId: string | null = null;
|
|
58
|
+
|
|
59
|
+
async function enqueueAudioToVideoTask(
|
|
60
|
+
projectId: string,
|
|
61
|
+
imagePath: string,
|
|
62
|
+
audioPath: string,
|
|
63
|
+
prompt: string,
|
|
64
|
+
stage1Steps: number,
|
|
65
|
+
frames: number,
|
|
66
|
+
): Promise<{ ok: boolean; error?: string; taskId?: string }> {
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(`${API_BASE}/api/queue/enqueue`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
projectId,
|
|
73
|
+
type: "audio-to-video",
|
|
74
|
+
label: "Audio to video",
|
|
75
|
+
payload: { imagePath, audioPath, prompt, stage1Steps, frames },
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
return { ok: false, error: await res.text() };
|
|
80
|
+
}
|
|
81
|
+
const task = (await res.json()) as { id?: string };
|
|
82
|
+
return { ok: true, taskId: task.id };
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { ok: false, error: String(e) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const useAudioToVideoStore = create<AudioToVideoStore>((set, get) => ({
|
|
89
|
+
image: null,
|
|
90
|
+
audio: null,
|
|
91
|
+
images: [],
|
|
92
|
+
audios: [],
|
|
93
|
+
imagesLoading: false,
|
|
94
|
+
audiosLoading: false,
|
|
95
|
+
uploadingImage: false,
|
|
96
|
+
uploadingAudio: false,
|
|
97
|
+
steps: 15,
|
|
98
|
+
duration: 1,
|
|
99
|
+
frames: 25,
|
|
100
|
+
prompt: "",
|
|
101
|
+
generating: false,
|
|
102
|
+
result: null,
|
|
103
|
+
error: null,
|
|
104
|
+
|
|
105
|
+
setImage: (image) => set({ image, error: null }),
|
|
106
|
+
setAudio: (audio) => set({ audio, error: null }),
|
|
107
|
+
setSteps: (steps) =>
|
|
108
|
+
set({ steps: Math.max(1, Math.round(Number(steps)) || 1), error: null }),
|
|
109
|
+
setFrames: (frames) =>
|
|
110
|
+
set({ frames: Math.max(1, Math.round(Number(frames)) || 1), error: null }),
|
|
111
|
+
setDuration: (duration) => {
|
|
112
|
+
const d = Math.max(0, Number(duration)) || 0;
|
|
113
|
+
set({
|
|
114
|
+
duration: d,
|
|
115
|
+
frames: Math.max(1, Math.round(d * 24) + 1),
|
|
116
|
+
error: null,
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
setPrompt: (prompt) => set({ prompt, error: null }),
|
|
120
|
+
clearResult: () => set({ result: null, error: null }),
|
|
121
|
+
|
|
122
|
+
fetchImages: async (projectId) => {
|
|
123
|
+
set({ imagesLoading: true });
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(`${API_BASE}/api/projects/${projectId}/images`);
|
|
126
|
+
if (!res.ok) throw new Error(await res.text());
|
|
127
|
+
const images: AssetFile[] = await res.json();
|
|
128
|
+
set({
|
|
129
|
+
images: images.map((i) => ({ ...i, url: resolveUrl(i.url) })),
|
|
130
|
+
imagesLoading: false,
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
set({ imagesLoading: false });
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
fetchAudios: async (projectId) => {
|
|
138
|
+
set({ audiosLoading: true });
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch(`${API_BASE}/api/projects/${projectId}/audios`);
|
|
141
|
+
if (!res.ok) throw new Error(await res.text());
|
|
142
|
+
const audios: AssetFile[] = await res.json();
|
|
143
|
+
set({
|
|
144
|
+
audios: audios.map((a) => ({ ...a, url: resolveUrl(a.url) })),
|
|
145
|
+
audiosLoading: false,
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
set({ audiosLoading: false });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
uploadImage: async (projectId, base64, filename) => {
|
|
153
|
+
set({ uploadingImage: true, error: null });
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(`${API_BASE}/api/upload/image`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "Content-Type": "application/json" },
|
|
158
|
+
body: JSON.stringify({ image: base64, filename, projectId }),
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
set({ uploadingImage: false, error: await res.text() });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
set({
|
|
166
|
+
uploadingImage: false,
|
|
167
|
+
image: {
|
|
168
|
+
filename: data.filename,
|
|
169
|
+
url: resolveUrl(
|
|
170
|
+
`/api/files?path=${encodeURIComponent(data.path)}`,
|
|
171
|
+
),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
void get().fetchImages(projectId);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
set({ uploadingImage: false, error: String(e) });
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
uploadAudio: async (projectId, base64, filename) => {
|
|
181
|
+
set({ uploadingAudio: true, error: null });
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetch(`${API_BASE}/api/upload/audio`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "Content-Type": "application/json" },
|
|
186
|
+
body: JSON.stringify({ audio: base64, filename, projectId }),
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
set({ uploadingAudio: false, error: await res.text() });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const data = await res.json();
|
|
193
|
+
set({
|
|
194
|
+
uploadingAudio: false,
|
|
195
|
+
audio: {
|
|
196
|
+
filename: data.filename,
|
|
197
|
+
url: resolveUrl(
|
|
198
|
+
`/api/files?path=${encodeURIComponent(data.path)}`,
|
|
199
|
+
),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
void get().fetchAudios(projectId);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
set({ uploadingAudio: false, error: String(e) });
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
generate: async (projectId) => {
|
|
209
|
+
const { image, audio, prompt, steps, frames, generating } = get();
|
|
210
|
+
if (generating || !image || !audio) return;
|
|
211
|
+
|
|
212
|
+
set({ generating: true, error: null, result: null });
|
|
213
|
+
|
|
214
|
+
const r = await enqueueAudioToVideoTask(
|
|
215
|
+
projectId,
|
|
216
|
+
image.filename,
|
|
217
|
+
audio.filename,
|
|
218
|
+
prompt.trim(),
|
|
219
|
+
steps,
|
|
220
|
+
frames,
|
|
221
|
+
);
|
|
222
|
+
audioToVideoActiveTaskId = r.taskId ?? null;
|
|
223
|
+
if (!r.ok) {
|
|
224
|
+
set({
|
|
225
|
+
generating: false,
|
|
226
|
+
error: r.error ?? "Failed to enqueue audio-to-video",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Reconcile the audio-to-video state with the latest queue task state. Only the
|
|
232
|
+
// task enqueued by this tab is reflected, so past tasks never surface stale results.
|
|
233
|
+
applyQueueTask: (task) => {
|
|
234
|
+
if (task.type !== "audio-to-video") return;
|
|
235
|
+
if (task.id !== audioToVideoActiveTaskId) return;
|
|
236
|
+
|
|
237
|
+
if (task.status === "completed") {
|
|
238
|
+
const url = task.result?.url;
|
|
239
|
+
set({
|
|
240
|
+
generating: false,
|
|
241
|
+
result: url ? resolveUrl(url) : null,
|
|
242
|
+
error: null,
|
|
243
|
+
});
|
|
244
|
+
} else if (task.status === "failed") {
|
|
245
|
+
set({ generating: false, error: task.error ?? "Audio-to-video failed" });
|
|
246
|
+
} else if (task.status === "cancelled" || task.status === "paused") {
|
|
247
|
+
set({ generating: false, error: null });
|
|
248
|
+
} else {
|
|
249
|
+
// pending / running
|
|
250
|
+
set({ generating: true, error: null });
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
reset: () => {
|
|
255
|
+
audioToVideoActiveTaskId = null;
|
|
256
|
+
set({
|
|
257
|
+
image: null,
|
|
258
|
+
audio: null,
|
|
259
|
+
images: [],
|
|
260
|
+
audios: [],
|
|
261
|
+
imagesLoading: false,
|
|
262
|
+
audiosLoading: false,
|
|
263
|
+
uploadingImage: false,
|
|
264
|
+
uploadingAudio: false,
|
|
265
|
+
steps: 15,
|
|
266
|
+
duration: 1,
|
|
267
|
+
frames: 25,
|
|
268
|
+
prompt: "",
|
|
269
|
+
generating: false,
|
|
270
|
+
result: null,
|
|
271
|
+
error: null,
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
}));
|