@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
@@ -2,20 +2,20 @@ import { useEffect, useState } from "react";
2
2
  import { useParams, useNavigate } from "react-router-dom";
3
3
  import { useProjectStore, type Project } from "../stores/projectStore";
4
4
  import { useGenerationStore } from "../stores/generationStore";
5
- import ExtendVideoTab from "./EditorTabs/ExtendVideoTab";
6
5
  import FastImageEditTab from "./EditorTabs/FastImageEditTab";
7
6
  import MovieStudioTab from "./EditorTabs/MovieStudioTab";
8
7
  import GenerateVideoTab from "./EditorTabs/GenerateVideoTab";
9
8
  import AgentTab from "./EditorTabs/AgentTab";
10
9
  import StoryWriterTab from "./EditorTabs/StoryWriterTab";
11
- import ExtractImageTab from "./EditorTabs/ExtractImageTab";
12
- import SceneVisualTab from "./EditorTabs/SceneVisualTab";
13
10
  import TextToImageTab from "./EditorTabs/TextToImageTab";
14
11
  import BatchVideoTab from "./EditorTabs/BatchVideoTab";
15
12
  import BatchImageToVideoTab from "./EditorTabs/BatchImageToVideoTab";
16
- import BatchVoiceVideoTab from "./EditorTabs/BatchVoiceVideoTab";
17
13
  import LlmServerTab from "./EditorTabs/LlmServerTab";
18
14
  import SetupAiModelTab from "./EditorTabs/SetupAiModelTab";
15
+ import UpscaleTab from "./EditorTabs/UpscaleTab";
16
+ import VoiceCloneTab from "./EditorTabs/VoiceCloneTab";
17
+ import AudioToVideoTab from "./EditorTabs/AudioToVideoTab";
18
+ import AdvancedVoiceCloneTab from "./EditorTabs/AdvancedVoiceCloneTab";
19
19
 
20
20
  export default function ProjectEditorPage() {
21
21
  const { id } = useParams<{ id: string }>();
@@ -87,24 +87,6 @@ export default function ProjectEditorPage() {
87
87
  </svg>
88
88
  );
89
89
 
90
- const ExtendIcon = (
91
- <svg
92
- width="18"
93
- height="18"
94
- viewBox="0 0 24 24"
95
- fill="none"
96
- stroke="currentColor"
97
- strokeWidth="2"
98
- strokeLinecap="round"
99
- strokeLinejoin="round"
100
- >
101
- <polygon points="23 7 16 12 23 17 23 7" />
102
- <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
103
- <line x1="5" y1="12" x2="11" y2="12" />
104
- <line x1="8" y1="9" x2="8" y2="15" />
105
- </svg>
106
- );
107
-
108
90
  const FastImageEditIcon = (
109
91
  <svg
110
92
  width="18"
@@ -172,7 +154,7 @@ export default function ProjectEditorPage() {
172
154
  </svg>
173
155
  );
174
156
 
175
- const ExtractIcon = (
157
+ const TextToImageIcon = (
176
158
  <svg
177
159
  width="18"
178
160
  height="18"
@@ -183,12 +165,11 @@ export default function ProjectEditorPage() {
183
165
  strokeLinecap="round"
184
166
  strokeLinejoin="round"
185
167
  >
186
- <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
187
- <circle cx="12" cy="13" r="4" />
168
+ <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" />
188
169
  </svg>
189
170
  );
190
171
 
191
- const SceneVisualIcon = (
172
+ const BatchVideoIcon = (
192
173
  <svg
193
174
  width="18"
194
175
  height="18"
@@ -200,12 +181,13 @@ export default function ProjectEditorPage() {
200
181
  strokeLinejoin="round"
201
182
  >
202
183
  <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
203
- <circle cx="8.5" cy="8.5" r="1.5" />
204
- <polyline points="21 15 16 10 5 21" />
184
+ <line x1="3" y1="9" x2="21" y2="9" />
185
+ <line x1="3" y1="15" x2="21" y2="15" />
186
+ <line x1="9" y1="3" x2="9" y2="21" />
205
187
  </svg>
206
188
  );
207
189
 
208
- const TextToImageIcon = (
190
+ const BatchImageToVideoIcon = (
209
191
  <svg
210
192
  width="18"
211
193
  height="18"
@@ -216,11 +198,13 @@ export default function ProjectEditorPage() {
216
198
  strokeLinecap="round"
217
199
  strokeLinejoin="round"
218
200
  >
219
- <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" />
201
+ <polygon points="12 2 2 7 12 12 22 7 12 2" />
202
+ <polyline points="2 17 12 22 22 17" />
203
+ <polyline points="2 12 12 17 22 12" />
220
204
  </svg>
221
205
  );
222
206
 
223
- const BatchVideoIcon = (
207
+ const ServerIcon = (
224
208
  <svg
225
209
  width="18"
226
210
  height="18"
@@ -231,14 +215,26 @@ export default function ProjectEditorPage() {
231
215
  strokeLinecap="round"
232
216
  strokeLinejoin="round"
233
217
  >
234
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
235
- <line x1="3" y1="9" x2="21" y2="9" />
236
- <line x1="3" y1="15" x2="21" y2="15" />
237
- <line x1="9" y1="3" x2="9" y2="21" />
218
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
219
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
220
+ <line x1="6" y1="6" x2="6.01" y2="6" />
221
+ <line x1="6" y1="18" x2="6.01" y2="18" />
238
222
  </svg>
239
223
  );
240
224
 
241
- const BatchVoiceIcon = (
225
+ const ServerStatusLight = () => (
226
+ <span
227
+ className={`w-2.5 h-2.5 rounded-full ${
228
+ store.agent.serverOnline === true
229
+ ? "bg-emerald-500 shadow-[0_0_6px_2px_rgba(16,185,129,0.5)]"
230
+ : store.agent.serverOnline === false
231
+ ? "bg-red-500 shadow-[0_0_6px_2px_rgba(239,68,68,0.5)]"
232
+ : "bg-tiffany-500"
233
+ }`}
234
+ />
235
+ );
236
+
237
+ const AiModelIcon = (
242
238
  <svg
243
239
  width="18"
244
240
  height="18"
@@ -249,13 +245,20 @@ export default function ProjectEditorPage() {
249
245
  strokeLinecap="round"
250
246
  strokeLinejoin="round"
251
247
  >
252
- <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
253
- <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
254
- <line x1="12" y1="19" x2="12" y2="23" />
248
+ <rect x="4" y="4" width="16" height="16" rx="2" ry="2" />
249
+ <rect x="9" y="9" width="6" height="6" />
250
+ <line x1="9" y1="1" x2="9" y2="4" />
251
+ <line x1="15" y1="1" x2="15" y2="4" />
252
+ <line x1="9" y1="20" x2="9" y2="23" />
253
+ <line x1="15" y1="20" x2="15" y2="23" />
254
+ <line x1="20" y1="9" x2="23" y2="9" />
255
+ <line x1="20" y1="14" x2="23" y2="14" />
256
+ <line x1="1" y1="9" x2="4" y2="9" />
257
+ <line x1="1" y1="14" x2="4" y2="14" />
255
258
  </svg>
256
259
  );
257
260
 
258
- const BatchImageToVideoIcon = (
261
+ const UpscaleIcon = (
259
262
  <svg
260
263
  width="18"
261
264
  height="18"
@@ -266,13 +269,14 @@ export default function ProjectEditorPage() {
266
269
  strokeLinecap="round"
267
270
  strokeLinejoin="round"
268
271
  >
269
- <polygon points="12 2 2 7 12 12 22 7 12 2" />
270
- <polyline points="2 17 12 22 22 17" />
271
- <polyline points="2 12 12 17 22 12" />
272
+ <polyline points="15 3 21 3 21 9" />
273
+ <polyline points="9 21 3 21 3 15" />
274
+ <line x1="21" y1="3" x2="14" y2="10" />
275
+ <line x1="3" y1="21" x2="10" y2="14" />
272
276
  </svg>
273
277
  );
274
278
 
275
- const ServerIcon = (
279
+ const VoiceCloneIcon = (
276
280
  <svg
277
281
  width="18"
278
282
  height="18"
@@ -283,26 +287,30 @@ export default function ProjectEditorPage() {
283
287
  strokeLinecap="round"
284
288
  strokeLinejoin="round"
285
289
  >
286
- <rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
287
- <rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
288
- <line x1="6" y1="6" x2="6.01" y2="6" />
289
- <line x1="6" y1="18" x2="6.01" y2="18" />
290
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
291
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
292
+ <line x1="12" y1="19" x2="12" y2="23" />
290
293
  </svg>
291
294
  );
292
295
 
293
- const ServerStatusLight = () => (
294
- <span
295
- className={`w-2.5 h-2.5 rounded-full ${
296
- store.agent.serverOnline === true
297
- ? "bg-emerald-500 shadow-[0_0_6px_2px_rgba(16,185,129,0.5)]"
298
- : store.agent.serverOnline === false
299
- ? "bg-red-500 shadow-[0_0_6px_2px_rgba(239,68,68,0.5)]"
300
- : "bg-tiffany-500"
301
- }`}
302
- />
296
+ const AudioToVideoIcon = (
297
+ <svg
298
+ width="18"
299
+ height="18"
300
+ viewBox="0 0 24 24"
301
+ fill="none"
302
+ stroke="currentColor"
303
+ strokeWidth="2"
304
+ strokeLinecap="round"
305
+ strokeLinejoin="round"
306
+ >
307
+ <polygon points="23 7 16 12 23 17 23 7" />
308
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
309
+ <line x1="4" y1="20" x2="8" y2="20" />
310
+ </svg>
303
311
  );
304
312
 
305
- const AiModelIcon = (
313
+ const AdvancedVoiceCloneIcon = (
306
314
  <svg
307
315
  width="18"
308
316
  height="18"
@@ -313,16 +321,11 @@ export default function ProjectEditorPage() {
313
321
  strokeLinecap="round"
314
322
  strokeLinejoin="round"
315
323
  >
316
- <rect x="4" y="4" width="16" height="16" rx="2" ry="2" />
317
- <rect x="9" y="9" width="6" height="6" />
318
- <line x1="9" y1="1" x2="9" y2="4" />
319
- <line x1="15" y1="1" x2="15" y2="4" />
320
- <line x1="9" y1="20" x2="9" y2="23" />
321
- <line x1="15" y1="20" x2="15" y2="23" />
322
- <line x1="20" y1="9" x2="23" y2="9" />
323
- <line x1="20" y1="14" x2="23" y2="14" />
324
- <line x1="1" y1="9" x2="4" y2="9" />
325
- <line x1="1" y1="14" x2="4" y2="14" />
324
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
325
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
326
+ <line x1="12" y1="19" x2="12" y2="23" />
327
+ <path d="M5 3v4" />
328
+ <path d="M3 5h4" />
326
329
  </svg>
327
330
  );
328
331
 
@@ -408,18 +411,7 @@ export default function ProjectEditorPage() {
408
411
  }`}
409
412
  >
410
413
  {VideoIcon}
411
- Generate Avatar Video
412
- </button>
413
- <button
414
- onClick={() => store.setActiveTab("extend")}
415
- className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
416
- store.activeTab === "extend"
417
- ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
418
- : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
419
- }`}
420
- >
421
- {ExtendIcon}
422
- Extend Video
414
+ Scene Video Generation
423
415
  </button>
424
416
  <button
425
417
  onClick={() => store.setActiveTab("fastImageEdit")}
@@ -455,82 +447,93 @@ export default function ProjectEditorPage() {
455
447
  Story Writer
456
448
  </button>
457
449
  <button
458
- onClick={() => store.setActiveTab("extract")}
450
+ onClick={() => store.setActiveTab("textToImage")}
459
451
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
460
- store.activeTab === "extract"
452
+ store.activeTab === "textToImage"
461
453
  ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
462
454
  : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
463
455
  }`}
464
456
  >
465
- {ExtractIcon}
466
- Extract Image
457
+ {TextToImageIcon}
458
+ Text-to-Image
467
459
  </button>
468
460
  <button
469
- onClick={() => store.setActiveTab("sceneVisual")}
461
+ onClick={() => store.setActiveTab("batchVideo")}
470
462
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
471
- store.activeTab === "sceneVisual"
463
+ store.activeTab === "batchVideo"
472
464
  ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
473
465
  : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
474
466
  }`}
475
467
  >
476
- {SceneVisualIcon}
477
- Scene Visual
468
+ {BatchVideoIcon}
469
+ Batch Video
478
470
  </button>
479
471
  <button
480
- onClick={() => store.setActiveTab("textToImage")}
472
+ onClick={() => store.setActiveTab("batchImageToVideo")}
481
473
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
482
- store.activeTab === "textToImage"
474
+ store.activeTab === "batchImageToVideo"
483
475
  ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
484
476
  : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
485
477
  }`}
486
478
  >
487
- {TextToImageIcon}
488
- Text-to-Image
479
+ {BatchImageToVideoIcon}
480
+ Batch Image to Video
489
481
  </button>
490
482
  <button
491
- onClick={() => store.setActiveTab("batchVideo")}
483
+ onClick={() => store.setActiveTab("llmServer")}
492
484
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
493
- store.activeTab === "batchVideo"
485
+ store.activeTab === "llmServer"
494
486
  ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
495
487
  : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
496
488
  }`}
497
489
  >
498
- {BatchVideoIcon}
499
- Batch Video
490
+ {ServerIcon}
491
+ LLM Server
492
+ <ServerStatusLight />
500
493
  </button>
501
494
  <button
502
- onClick={() => store.setActiveTab("batchImageToVideo")}
495
+ onClick={() => store.setActiveTab("upscale")}
503
496
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
504
- store.activeTab === "batchImageToVideo"
497
+ store.activeTab === "upscale"
505
498
  ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
506
499
  : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
507
500
  }`}
508
501
  >
509
- {BatchImageToVideoIcon}
510
- Batch Image to Video
502
+ {UpscaleIcon}
503
+ Upscale Media
511
504
  </button>
512
505
  <button
513
- onClick={() => store.setActiveTab("batchVoice")}
506
+ onClick={() => store.setActiveTab("voiceClone")}
514
507
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
515
- store.activeTab === "batchVoice"
508
+ store.activeTab === "voiceClone"
516
509
  ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
517
510
  : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
518
511
  }`}
519
512
  >
520
- {BatchVoiceIcon}
521
- Batch Videos with Voice Over
513
+ {VoiceCloneIcon}
514
+ Voice Clone
522
515
  </button>
523
516
  <button
524
- onClick={() => store.setActiveTab("llmServer")}
517
+ onClick={() => store.setActiveTab("advancedVoiceClone")}
525
518
  className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
526
- store.activeTab === "llmServer"
519
+ store.activeTab === "advancedVoiceClone"
527
520
  ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
528
521
  : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
529
522
  }`}
530
523
  >
531
- {ServerIcon}
532
- LLM Server
533
- <ServerStatusLight />
524
+ {AdvancedVoiceCloneIcon}
525
+ Advanced Voice Clone
526
+ </button>
527
+ <button
528
+ onClick={() => store.setActiveTab("audioToVideo")}
529
+ className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
530
+ store.activeTab === "audioToVideo"
531
+ ? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
532
+ : "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
533
+ }`}
534
+ >
535
+ {AudioToVideoIcon}
536
+ Audio to Video
534
537
  </button>
535
538
  <button
536
539
  onClick={() => store.setActiveTab("aiModels")}
@@ -558,9 +561,6 @@ export default function ProjectEditorPage() {
558
561
  {/* ========== VIDEO GENERATION PANEL ========== */}
559
562
  {store.activeTab === "video" && <GenerateVideoTab projectId={id!} />}
560
563
 
561
- {/* ========== EXTEND VIDEO PANEL ========== */}
562
- {store.activeTab === "extend" && <ExtendVideoTab projectId={id!} />}
563
-
564
564
  {/* ========== AGENT PANEL ========== */}
565
565
  {store.activeTab === "agent" && <AgentTab projectId={id!} />}
566
566
 
@@ -569,14 +569,6 @@ export default function ProjectEditorPage() {
569
569
  <StoryWriterTab projectId={id!} />
570
570
  )}
571
571
 
572
- {/* ========== EXTRACT IMAGE PANEL ========== */}
573
- {store.activeTab === "extract" && <ExtractImageTab projectId={id!} />}
574
-
575
- {/* ========== SCENE VISUAL PANEL ========== */}
576
- {store.activeTab === "sceneVisual" && (
577
- <SceneVisualTab projectId={id!} />
578
- )}
579
-
580
572
  {/* ========== TEXT-TO-IMAGE PANEL ========== */}
581
573
  {store.activeTab === "textToImage" && (
582
574
  <TextToImageTab projectId={id!} />
@@ -592,16 +584,29 @@ export default function ProjectEditorPage() {
592
584
  <BatchImageToVideoTab projectId={id!} />
593
585
  )}
594
586
 
595
- {/* ========== BATCH VOICE VIDEO PANEL ========== */}
596
- {store.activeTab === "batchVoice" && (
597
- <BatchVoiceVideoTab projectId={id!} />
598
- )}
599
-
600
587
  {/* ========== LLM SERVER PANEL ========== */}
601
588
  {store.activeTab === "llmServer" && <LlmServerTab />}
602
589
 
603
590
  {/* ========== SETUP AI MODELS PANEL ========== */}
604
591
  {store.activeTab === "aiModels" && <SetupAiModelTab />}
592
+
593
+ {/* ========== UPSCALE MEDIA PANEL ========== */}
594
+ {store.activeTab === "upscale" && <UpscaleTab projectId={id!} />}
595
+
596
+ {/* ========== VOICE CLONE PANEL ========== */}
597
+ {store.activeTab === "voiceClone" && (
598
+ <VoiceCloneTab projectId={id!} />
599
+ )}
600
+
601
+ {/* ========== ADVANCED VOICE CLONE PANEL ========== */}
602
+ {store.activeTab === "advancedVoiceClone" && (
603
+ <AdvancedVoiceCloneTab projectId={id!} />
604
+ )}
605
+
606
+ {/* ========== AUDIO TO VIDEO PANEL ========== */}
607
+ {store.activeTab === "audioToVideo" && (
608
+ <AudioToVideoTab projectId={id!} />
609
+ )}
605
610
  </div>
606
611
  </div>
607
612
  </div>
@@ -0,0 +1,207 @@
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 VoiceAudio {
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 AdvancedVoiceCloneStore {
16
+ text: string;
17
+ language: string;
18
+ outPrefix: string;
19
+ model: string;
20
+ refAudio: VoiceAudio | null;
21
+ audios: VoiceAudio[];
22
+ audiosLoading: boolean;
23
+ uploading: boolean;
24
+ generating: boolean;
25
+ result: string | null;
26
+ error: string | null;
27
+
28
+ setText: (t: string) => void;
29
+ setLanguage: (l: string) => void;
30
+ setOutPrefix: (p: string) => void;
31
+ setModel: (m: string) => void;
32
+ setRefAudio: (a: VoiceAudio | null) => void;
33
+ clearResult: () => void;
34
+ fetchAudios: (projectId: string) => Promise<void>;
35
+ uploadAudio: (
36
+ projectId: string,
37
+ base64: string,
38
+ filename: string,
39
+ ) => Promise<void>;
40
+ generate: (projectId: string) => Promise<void>;
41
+ applyQueueTask: (task: QueueTask) => void;
42
+ reset: () => void;
43
+ }
44
+
45
+ /** The queue task id enqueued by the current generate action, if any. */
46
+ let advancedVoiceCloneActiveTaskId: string | null = null;
47
+
48
+ async function enqueueAdvancedVoiceCloneTask(
49
+ projectId: string,
50
+ text: string,
51
+ refAudioPath: string,
52
+ language: string,
53
+ outPrefix: string,
54
+ model: string,
55
+ ): Promise<{ ok: boolean; error?: string; taskId?: string }> {
56
+ try {
57
+ const res = await fetch(`${API_BASE}/api/queue/enqueue`, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({
61
+ projectId,
62
+ type: "advanced-voice-clone",
63
+ label: "Advanced voice clone",
64
+ payload: { text, refAudioPath, language, outPrefix, model },
65
+ }),
66
+ });
67
+ if (!res.ok) {
68
+ return { ok: false, error: await res.text() };
69
+ }
70
+ const task = (await res.json()) as { id?: string };
71
+ return { ok: true, taskId: task.id };
72
+ } catch (e) {
73
+ return { ok: false, error: String(e) };
74
+ }
75
+ }
76
+
77
+ export const useAdvancedVoiceCloneStore = create<AdvancedVoiceCloneStore>(
78
+ (set, get) => ({
79
+ text: "",
80
+ language: "YUE",
81
+ outPrefix: "voice",
82
+ model: "./dots-tts-mlx-weights/mf-int4",
83
+ refAudio: null,
84
+ audios: [],
85
+ audiosLoading: false,
86
+ uploading: false,
87
+ generating: false,
88
+ result: null,
89
+ error: null,
90
+
91
+ setText: (text) => set({ text, error: null }),
92
+ setLanguage: (language) => set({ language, error: null }),
93
+ setOutPrefix: (outPrefix) => set({ outPrefix, error: null }),
94
+ setModel: (model) => set({ model, error: null }),
95
+ setRefAudio: (refAudio) => set({ refAudio, error: null }),
96
+ clearResult: () => set({ result: null, error: null }),
97
+
98
+ fetchAudios: async (projectId) => {
99
+ set({ audiosLoading: true });
100
+ try {
101
+ const res = await fetch(`${API_BASE}/api/projects/${projectId}/audios`);
102
+ if (!res.ok) throw new Error(await res.text());
103
+ const audios: VoiceAudio[] = await res.json();
104
+ set({
105
+ audios: audios.map((a) => ({ ...a, url: resolveUrl(a.url) })),
106
+ audiosLoading: false,
107
+ });
108
+ } catch {
109
+ set({ audiosLoading: false });
110
+ }
111
+ },
112
+
113
+ uploadAudio: async (projectId, base64, filename) => {
114
+ set({ uploading: true, error: null });
115
+ try {
116
+ const res = await fetch(`${API_BASE}/api/upload/audio`, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify({ audio: base64, filename, projectId }),
120
+ });
121
+ if (!res.ok) {
122
+ set({ uploading: false, error: await res.text() });
123
+ return;
124
+ }
125
+ const data = await res.json();
126
+ set({
127
+ uploading: false,
128
+ refAudio: {
129
+ filename: data.filename,
130
+ url: resolveUrl(
131
+ `/api/files?path=${encodeURIComponent(data.path)}`,
132
+ ),
133
+ },
134
+ });
135
+ void get().fetchAudios(projectId);
136
+ } catch (e) {
137
+ set({ uploading: false, error: String(e) });
138
+ }
139
+ },
140
+
141
+ generate: async (projectId) => {
142
+ const { text, refAudio, language, outPrefix, model, generating } = get();
143
+ if (generating || !refAudio || !text.trim()) return;
144
+
145
+ set({ generating: true, error: null, result: null });
146
+
147
+ const r = await enqueueAdvancedVoiceCloneTask(
148
+ projectId,
149
+ text.trim(),
150
+ refAudio.filename,
151
+ language,
152
+ outPrefix,
153
+ model,
154
+ );
155
+ advancedVoiceCloneActiveTaskId = r.taskId ?? null;
156
+ if (!r.ok) {
157
+ set({
158
+ generating: false,
159
+ error: r.error ?? "Failed to enqueue advanced voice clone",
160
+ });
161
+ }
162
+ },
163
+
164
+ // Reconcile state with the latest queue task state. Only the task enqueued by
165
+ // this tab is reflected, so past tasks never surface stale results.
166
+ applyQueueTask: (task) => {
167
+ if (task.type !== "advanced-voice-clone") return;
168
+ if (task.id !== advancedVoiceCloneActiveTaskId) return;
169
+
170
+ if (task.status === "completed") {
171
+ const url = task.result?.url;
172
+ set({
173
+ generating: false,
174
+ result: url ? resolveUrl(url) : null,
175
+ error: null,
176
+ });
177
+ } else if (task.status === "failed") {
178
+ set({
179
+ generating: false,
180
+ error: task.error ?? "Advanced voice clone failed",
181
+ });
182
+ } else if (task.status === "cancelled" || task.status === "paused") {
183
+ set({ generating: false, error: null });
184
+ } else {
185
+ // pending / running
186
+ set({ generating: true, error: null });
187
+ }
188
+ },
189
+
190
+ reset: () => {
191
+ advancedVoiceCloneActiveTaskId = null;
192
+ set({
193
+ text: "",
194
+ language: "YUE",
195
+ outPrefix: "voice",
196
+ model: "./dots-tts-mlx-weights/mf-int4",
197
+ refAudio: null,
198
+ audios: [],
199
+ audiosLoading: false,
200
+ uploading: false,
201
+ generating: false,
202
+ result: null,
203
+ error: null,
204
+ });
205
+ },
206
+ }),
207
+ );