@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.
Files changed (32) hide show
  1. package/dist/backend/movie-backend/agent/prompt/ltx.txt +20 -0
  2. package/dist/backend/movie-backend/agent/prompt/script.md +111 -1
  3. package/dist/backend/movie-backend/core.js +41 -0
  4. package/dist/backend/movie-backend/generation-queue.d.ts +1 -1
  5. package/dist/backend/movie-backend/generation-queue.js +75 -8
  6. package/dist/backend/movie-backend/render-media.d.ts +79 -2
  7. package/dist/backend/movie-backend/render-media.js +694 -418
  8. package/frontend/src/movie-app/components/EditorTabs/AdvancedVoiceCloneTab.tsx +366 -0
  9. package/frontend/src/movie-app/components/EditorTabs/AudioToVideoTab.tsx +406 -0
  10. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +47 -0
  11. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +155 -1
  12. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +33 -13
  13. package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +26 -5
  14. package/frontend/src/movie-app/components/EditorTabs/UpscaleTab.tsx +343 -0
  15. package/frontend/src/movie-app/components/EditorTabs/VoiceCloneTab.tsx +365 -0
  16. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +130 -125
  17. package/frontend/src/movie-app/stores/advancedVoiceCloneStore.ts +205 -0
  18. package/frontend/src/movie-app/stores/aiModelStore.ts +25 -2
  19. package/frontend/src/movie-app/stores/audioToVideoStore.ts +274 -0
  20. package/frontend/src/movie-app/stores/generationStore.ts +156 -255
  21. package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
  22. package/frontend/src/movie-app/stores/queueStore.ts +47 -1
  23. package/frontend/src/movie-app/stores/upscaleStore.ts +118 -0
  24. package/frontend/src/movie-app/stores/voiceCloneStore.ts +227 -0
  25. package/package.json +1 -1
  26. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +0 -913
  27. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +0 -305
  28. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +0 -249
  29. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +0 -267
  30. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +0 -75
  31. package/frontend/src/movie-app/stores/batchVoiceStore.ts +0 -990
  32. package/frontend/src/movie-app/stores/sceneVisualStore.ts +0 -251
@@ -0,0 +1,366 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useQueueStore } from "../../stores/queueStore";
3
+ import { useProjectStore } from "../../stores/projectStore";
4
+ import { useAiModelStore } from "../../stores/aiModelStore";
5
+ import { useAdvancedVoiceCloneStore } from "../../stores/advancedVoiceCloneStore";
6
+ import TaskQueuePanel from "./TaskQueuePanel";
7
+ import TerminalLogPanel from "./TerminalLogPanel";
8
+
9
+ interface Props {
10
+ projectId: string;
11
+ }
12
+
13
+ export default function AdvancedVoiceCloneTab({ projectId }: Props) {
14
+ const queue = useQueueStore();
15
+ const { openFolder } = useProjectStore();
16
+ const ai = useAiModelStore();
17
+ const avc = useAdvancedVoiceCloneStore();
18
+
19
+ const fileInputRef = useRef<HTMLInputElement>(null);
20
+
21
+ useEffect(() => {
22
+ avc.fetchAudios(projectId);
23
+ ai.checkStatus();
24
+ // eslint-disable-next-line react-hooks/exhaustive-deps
25
+ }, [projectId]);
26
+
27
+ // Stream the generation queue so advanced voice-clone tasks surface here.
28
+ useEffect(() => {
29
+ queue.startStreaming(projectId);
30
+ return () => queue.stopStreaming();
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, [projectId]);
33
+
34
+ // Reconcile state with the latest queue task state.
35
+ useEffect(() => {
36
+ for (const task of queue.tasks) avc.applyQueueTask(task);
37
+ // eslint-disable-next-line react-hooks/exhaustive-deps
38
+ }, [queue.tasks, projectId]);
39
+
40
+ const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
41
+ const file = e.target.files?.[0];
42
+ e.target.value = "";
43
+ if (!file) return;
44
+ const reader = new FileReader();
45
+ reader.onload = () => {
46
+ avc.uploadAudio(projectId, reader.result as string, file.name);
47
+ };
48
+ reader.readAsDataURL(file);
49
+ };
50
+
51
+ // ========== SVG Icons ==========
52
+
53
+ const MicIcon = (
54
+ <svg
55
+ width="18"
56
+ height="18"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ strokeWidth="2"
61
+ strokeLinecap="round"
62
+ strokeLinejoin="round"
63
+ >
64
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
65
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
66
+ <line x1="12" y1="19" x2="12" y2="23" />
67
+ </svg>
68
+ );
69
+
70
+ const UploadIcon = (
71
+ <svg
72
+ width="14"
73
+ height="14"
74
+ viewBox="0 0 24 24"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ strokeWidth="2"
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ >
81
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
82
+ <polyline points="17 8 12 3 7 8" />
83
+ <line x1="12" y1="3" x2="12" y2="15" />
84
+ </svg>
85
+ );
86
+
87
+ const SparkleIcon = (
88
+ <svg
89
+ width="16"
90
+ height="16"
91
+ viewBox="0 0 24 24"
92
+ fill="none"
93
+ stroke="currentColor"
94
+ strokeWidth="2"
95
+ >
96
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
97
+ </svg>
98
+ );
99
+
100
+ const SpinnerIcon = (
101
+ <svg
102
+ className="animate-spin text-tiffany-600"
103
+ width="16"
104
+ height="16"
105
+ viewBox="0 0 24 24"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ strokeWidth="2"
109
+ >
110
+ <circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
111
+ <path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
112
+ </svg>
113
+ );
114
+
115
+ const FolderIcon = (
116
+ <svg
117
+ width="16"
118
+ height="16"
119
+ viewBox="0 0 24 24"
120
+ fill="none"
121
+ stroke="currentColor"
122
+ strokeWidth="2"
123
+ strokeLinecap="round"
124
+ strokeLinejoin="round"
125
+ >
126
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
127
+ </svg>
128
+ );
129
+
130
+ const DownloadIcon = (
131
+ <svg
132
+ width="16"
133
+ height="16"
134
+ viewBox="0 0 24 24"
135
+ fill="none"
136
+ stroke="currentColor"
137
+ strokeWidth="2"
138
+ strokeLinecap="round"
139
+ strokeLinejoin="round"
140
+ >
141
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
142
+ <polyline points="7 10 12 15 17 10" />
143
+ <line x1="12" y1="15" x2="12" y2="3" />
144
+ </svg>
145
+ );
146
+
147
+ const inputClass =
148
+ "w-full px-4 py-2.5 bg-ink-50 border border-ink-200 rounded-2xl text-ink-900 text-sm placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all disabled:opacity-50";
149
+
150
+ return (
151
+ <div className="flex flex-col gap-7">
152
+ <div className="flex items-center gap-2">
153
+ <span className="text-tiffany-600">{MicIcon}</span>
154
+ <h2 className="text-base font-semibold text-ink-900">
155
+ Advanced Voice Clone
156
+ </h2>
157
+ </div>
158
+
159
+ {/* ===== Model download ===== */}
160
+ <div className="flex flex-col gap-2 rounded-2xl border border-ink-200 bg-white p-4">
161
+ <div className="flex items-center gap-1.5 text-xs font-medium">
162
+ {ai.dotsTtsDownloaded === null ? (
163
+ <span className="inline-flex items-center gap-1.5 text-ink-500">
164
+ {SpinnerIcon}
165
+ Checking…
166
+ </span>
167
+ ) : ai.dotsTtsDownloaded ? (
168
+ <span className="text-emerald-600">✓ dots-tts model downloaded</span>
169
+ ) : (
170
+ <span className="text-amber-600">dots-tts model not downloaded</span>
171
+ )}
172
+ </div>
173
+ <button
174
+ onClick={() => ai.downloadModel("dots-tts")}
175
+ disabled={ai.downloading !== null}
176
+ className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-tiffany-500 hover:bg-tiffany-600 active:bg-tiffany-700 disabled:bg-ink-200 disabled:text-ink-500 text-ink-950 text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm hover:shadow-md disabled:shadow-none"
177
+ >
178
+ {ai.downloading === "dots-tts" ? SpinnerIcon : DownloadIcon}
179
+ {ai.downloading === "dots-tts"
180
+ ? "Downloading…"
181
+ : "Download dots-tts Model"}
182
+ </button>
183
+ </div>
184
+
185
+ <TaskQueuePanel projectId={projectId} />
186
+ <TerminalLogPanel />
187
+
188
+ {/* ===== Reference voice ===== */}
189
+ <div>
190
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
191
+ Reference Voice
192
+ </label>
193
+ <div className="flex items-center gap-2">
194
+ <button
195
+ onClick={() => fileInputRef.current?.click()}
196
+ disabled={avc.uploading || avc.generating}
197
+ className="flex items-center gap-1.5 px-4 py-2 text-xs font-medium rounded-xl border border-ink-200 bg-white text-ink-600 hover:border-ink-300 disabled:opacity-50 transition-colors"
198
+ >
199
+ {UploadIcon}
200
+ Upload Reference Voice
201
+ </button>
202
+ <input
203
+ ref={fileInputRef}
204
+ type="file"
205
+ accept="audio/*"
206
+ onChange={handleUpload}
207
+ className="hidden"
208
+ />
209
+ {avc.uploading && (
210
+ <span className="text-xs text-ink-600 italic">Uploading…</span>
211
+ )}
212
+ </div>
213
+
214
+ {avc.refAudio && (
215
+ <div className="mt-3 flex items-center gap-3 rounded-2xl border border-ink-200 bg-ink-50 p-3">
216
+ <audio src={avc.refAudio.url} controls className="h-9" />
217
+ <span className="truncate text-xs text-ink-600">
218
+ {avc.refAudio.filename}
219
+ </span>
220
+ </div>
221
+ )}
222
+
223
+ <div className="mt-3">
224
+ {avc.audiosLoading ? (
225
+ <p className="text-xs text-ink-500 italic py-3 text-center">
226
+ Loading voices…
227
+ </p>
228
+ ) : avc.audios.length === 0 ? (
229
+ <p className="text-xs text-ink-500 italic py-3 text-center border border-dashed border-ink-200 rounded-2xl">
230
+ No uploaded voices yet. Upload one above.
231
+ </p>
232
+ ) : (
233
+ <ul className="divide-y divide-ink-200 border border-ink-200 rounded-2xl overflow-hidden max-h-48 overflow-y-auto">
234
+ {avc.audios.map((a) => {
235
+ const isSelected = avc.refAudio?.filename === a.filename;
236
+ return (
237
+ <li key={a.filename}>
238
+ <button
239
+ onClick={() => avc.setRefAudio(a)}
240
+ disabled={avc.generating}
241
+ className={`w-full flex items-center gap-2 px-3 py-2 text-left text-xs transition-colors disabled:opacity-50 ${
242
+ isSelected
243
+ ? "bg-ink-100 text-ink-800"
244
+ : "text-ink-600 hover:bg-ink-100"
245
+ }`}
246
+ >
247
+ <span className="text-ink-500">{MicIcon}</span>
248
+ <span className="truncate">{a.filename}</span>
249
+ </button>
250
+ </li>
251
+ );
252
+ })}
253
+ </ul>
254
+ )}
255
+ </div>
256
+ </div>
257
+
258
+ {/* ===== Model ===== */}
259
+ <div>
260
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
261
+ Model
262
+ </label>
263
+ <input
264
+ type="text"
265
+ value={avc.model}
266
+ onChange={(e) => avc.setModel(e.target.value)}
267
+ disabled={avc.generating}
268
+ className={inputClass}
269
+ />
270
+ </div>
271
+
272
+ {/* ===== Language ===== */}
273
+ <div>
274
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
275
+ Language
276
+ </label>
277
+ <input
278
+ type="text"
279
+ value={avc.language}
280
+ onChange={(e) => avc.setLanguage(e.target.value)}
281
+ placeholder="e.g. YUE"
282
+ disabled={avc.generating}
283
+ className={inputClass}
284
+ />
285
+ </div>
286
+
287
+ {/* ===== Output prefix ===== */}
288
+ <div>
289
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
290
+ Output Prefix
291
+ </label>
292
+ <input
293
+ type="text"
294
+ value={avc.outPrefix}
295
+ onChange={(e) => avc.setOutPrefix(e.target.value)}
296
+ placeholder="voice"
297
+ disabled={avc.generating}
298
+ className={inputClass}
299
+ />
300
+ <p className="text-xs text-ink-600/50 mt-1.5">
301
+ Output file: {avc.outPrefix || "voice"}_000.wav
302
+ </p>
303
+ </div>
304
+
305
+ {/* ===== Text ===== */}
306
+ <div>
307
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
308
+ Text
309
+ </label>
310
+ <textarea
311
+ value={avc.text}
312
+ onChange={(e) => avc.setText(e.target.value)}
313
+ placeholder="What should the cloned voice say?"
314
+ rows={5}
315
+ disabled={avc.generating}
316
+ className="w-full px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl text-ink-900 text-sm placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all resize-none disabled:opacity-50"
317
+ />
318
+ </div>
319
+
320
+ {/* ===== Generate ===== */}
321
+ {avc.generating ? (
322
+ <div className="flex items-center gap-2 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
323
+ {SpinnerIcon}
324
+ <span className="text-sm font-medium text-ink-700">
325
+ Generating voice…
326
+ </span>
327
+ </div>
328
+ ) : (
329
+ <button
330
+ onClick={() => avc.generate(projectId)}
331
+ disabled={!avc.refAudio || !avc.text.trim()}
332
+ className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-tiffany-500 hover:bg-tiffany-600 active:bg-tiffany-700 disabled:bg-ink-200 disabled:text-ink-500 text-ink-950 text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm hover:shadow-md disabled:shadow-none"
333
+ >
334
+ {SparkleIcon}
335
+ Generate Voice
336
+ </button>
337
+ )}
338
+
339
+ {/* ===== Open output folder ===== */}
340
+ <button
341
+ onClick={() => openFolder(projectId, "output")}
342
+ className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-ink-50 hover:bg-ink-200 text-ink-700 text-sm font-medium rounded-2xl border border-ink-200 transition-colors"
343
+ >
344
+ {FolderIcon}
345
+ Open Output Folder
346
+ </button>
347
+
348
+ {/* ===== Error ===== */}
349
+ {avc.error && (
350
+ <div className="p-5 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
351
+ {avc.error}
352
+ </div>
353
+ )}
354
+
355
+ {/* ===== Result ===== */}
356
+ {avc.result && (
357
+ <div>
358
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
359
+ Generated Voice
360
+ </label>
361
+ <audio src={avc.result} controls className="w-full" />
362
+ </div>
363
+ )}
364
+ </div>
365
+ );
366
+ }