@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.
Files changed (33) hide show
  1. package/dist/backend/movie-backend/agent/prompt/script.md +213 -0
  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 +82 -6
  5. package/dist/backend/movie-backend/render-media.d.ts +89 -1
  6. package/dist/backend/movie-backend/render-media.js +772 -476
  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 +67 -51
  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 +131 -126
  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 +250 -345
  20. package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
  21. package/frontend/src/movie-app/stores/projectStore.ts +7 -3
  22. package/frontend/src/movie-app/stores/queueStore.ts +48 -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/CharacterSheet.tsx +0 -234
  28. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +0 -305
  29. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +0 -249
  30. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +0 -267
  31. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +0 -75
  32. package/frontend/src/movie-app/stores/batchVoiceStore.ts +0 -990
  33. 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
+ }