@effectnode/media 0.8.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.
- package/dist/backend/movie-backend/agent/prompt/script.md +213 -0
- package/dist/backend/movie-backend/core.js +33 -0
- package/dist/backend/movie-backend/generation-queue.d.ts +1 -1
- package/dist/backend/movie-backend/generation-queue.js +82 -6
- package/dist/backend/movie-backend/render-media.d.ts +89 -1
- package/dist/backend/movie-backend/render-media.js +772 -476
- 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 +67 -51
- 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 +342 -0
- package/frontend/src/movie-app/components/EditorTabs/VoiceCloneTab.tsx +365 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +131 -126
- package/frontend/src/movie-app/stores/advancedVoiceCloneStore.ts +207 -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 +250 -345
- package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
- package/frontend/src/movie-app/stores/projectStore.ts +7 -3
- package/frontend/src/movie-app/stores/queueStore.ts +48 -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/CharacterSheet.tsx +0 -234
- 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
|
@@ -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
|
+
}
|