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