@effectnode/media 0.9.0 → 0.10.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 (31) hide show
  1. package/dist/backend/movie-backend/agent/prompt/script.md +111 -1
  2. package/dist/backend/movie-backend/core.js +33 -0
  3. package/dist/backend/movie-backend/generation-queue.d.ts +1 -1
  4. package/dist/backend/movie-backend/generation-queue.js +75 -8
  5. package/dist/backend/movie-backend/render-media.d.ts +79 -2
  6. package/dist/backend/movie-backend/render-media.js +722 -418
  7. package/frontend/src/movie-app/components/EditorTabs/AdvancedVoiceCloneTab.tsx +366 -0
  8. package/frontend/src/movie-app/components/EditorTabs/AudioToVideoTab.tsx +406 -0
  9. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +47 -0
  10. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +155 -1
  11. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +33 -13
  12. package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +26 -5
  13. package/frontend/src/movie-app/components/EditorTabs/UpscaleTab.tsx +342 -0
  14. package/frontend/src/movie-app/components/EditorTabs/VoiceCloneTab.tsx +365 -0
  15. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +130 -125
  16. package/frontend/src/movie-app/stores/advancedVoiceCloneStore.ts +207 -0
  17. package/frontend/src/movie-app/stores/aiModelStore.ts +25 -2
  18. package/frontend/src/movie-app/stores/audioToVideoStore.ts +274 -0
  19. package/frontend/src/movie-app/stores/generationStore.ts +156 -255
  20. package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
  21. package/frontend/src/movie-app/stores/queueStore.ts +47 -1
  22. package/frontend/src/movie-app/stores/upscaleStore.ts +118 -0
  23. package/frontend/src/movie-app/stores/voiceCloneStore.ts +227 -0
  24. package/package.json +1 -1
  25. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +0 -913
  26. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +0 -305
  27. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +0 -249
  28. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +0 -267
  29. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +0 -75
  30. package/frontend/src/movie-app/stores/batchVoiceStore.ts +0 -990
  31. 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 = "z-image" | "flux" | "ltx" | "tts" | "gemma";
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
+ }));