@effectnode/media 0.9.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 (31) hide show
  1. package/dist/backend/movie-backend/agent/prompt/script.md +111 -1
  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 +75 -8
  5. package/dist/backend/movie-backend/render-media.d.ts +79 -2
  6. package/dist/backend/movie-backend/render-media.js +722 -418
  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 +47 -0
  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 +130 -125
  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 +156 -255
  20. package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
  21. package/frontend/src/movie-app/stores/queueStore.ts +47 -1
  22. package/frontend/src/movie-app/stores/upscaleStore.ts +118 -0
  23. package/frontend/src/movie-app/stores/voiceCloneStore.ts +227 -0
  24. package/package.json +1 -1
  25. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +0 -913
  26. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +0 -305
  27. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +0 -249
  28. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +0 -267
  29. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +0 -75
  30. package/frontend/src/movie-app/stores/batchVoiceStore.ts +0 -990
  31. package/frontend/src/movie-app/stores/sceneVisualStore.ts +0 -251
@@ -909,20 +909,40 @@ export default function MovieStudioTab({ projectId }: Props) {
909
909
  <h3 className="text-sm font-semibold text-ink-900">
910
910
  Scene Images
911
911
  </h3>
912
- {store.sceneImagesRendering ? (
913
- <span className="flex items-center gap-1.5 text-xs text-tiffany-600">
914
- {SpinnerIcon}
915
- {store.sceneImageStatus ?? "Rendering..."}
916
- </span>
917
- ) : (
918
- <button
919
- onClick={() => store.renderSceneImages(projectId)}
920
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
912
+ <div className="flex items-center gap-2">
913
+ <label
914
+ htmlFor="scene-image-steps"
915
+ className="text-xs font-medium text-ink-600"
921
916
  >
922
- {SparkleIcon}
923
- Render Scene Images
924
- </button>
925
- )}
917
+ Steps
918
+ </label>
919
+ <input
920
+ id="scene-image-steps"
921
+ type="number"
922
+ min={1}
923
+ step={1}
924
+ value={store.sceneImageSteps}
925
+ onChange={(e) =>
926
+ store.setSceneImageSteps(Number(e.target.value))
927
+ }
928
+ disabled={store.sceneImagesRendering}
929
+ className="w-20 px-2 py-1.5 text-xs bg-ink-50 border border-ink-200 rounded-lg text-ink-800 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/25 disabled:opacity-50"
930
+ />
931
+ {store.sceneImagesRendering ? (
932
+ <span className="flex items-center gap-1.5 text-xs text-tiffany-600">
933
+ {SpinnerIcon}
934
+ {store.sceneImageStatus ?? "Rendering..."}
935
+ </span>
936
+ ) : (
937
+ <button
938
+ onClick={() => store.renderSceneImages(projectId)}
939
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
940
+ >
941
+ {SparkleIcon}
942
+ Render Scene Images
943
+ </button>
944
+ )}
945
+ </div>
926
946
  </div>
927
947
 
928
948
  {store.sceneImagesRendering &&
@@ -190,11 +190,21 @@ const DOWNLOAD_MODELS: {
190
190
  name: "AbstractFramework/flux.2-klein-4b-8bit",
191
191
  desc: "Character, place & scene images",
192
192
  },
193
+ {
194
+ id: "seedvr2",
195
+ name: "AbstractFramework/seedvr2-7b-8bit",
196
+ desc: "Video generation (SeedVR2)",
197
+ },
193
198
  {
194
199
  id: "ltx",
195
200
  name: "dgrauet/ltx-2.3-mlx-q8",
196
201
  desc: "Video generation",
197
202
  },
203
+ {
204
+ id: "ltx-base",
205
+ name: "dgrauet/ltx-2.3-mlx",
206
+ desc: "Video generation (full precision)",
207
+ },
198
208
  {
199
209
  id: "tts",
200
210
  name: "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
@@ -205,6 +215,11 @@ const DOWNLOAD_MODELS: {
205
215
  name: "mlx-community/gemma-4-e4b-it-8bit",
206
216
  desc: "Story planning & dialogue (LLM)",
207
217
  },
218
+ {
219
+ id: "dots-tts",
220
+ name: "shraey/dots-tts-mlx (mf-int4)",
221
+ desc: "Advanced voice clone (dots-tts)",
222
+ },
208
223
  ];
209
224
 
210
225
  export default function SetupAiModelTab() {
@@ -301,11 +316,17 @@ export default function SetupAiModelTab() {
301
316
  ? store.zImageDownloaded
302
317
  : m.id === "flux"
303
318
  ? store.fluxDownloaded
304
- : m.id === "ltx"
305
- ? store.ltxDownloaded
306
- : m.id === "gemma"
307
- ? store.gemmaDownloaded
308
- : store.ttsDownloaded;
319
+ : m.id === "seedvr2"
320
+ ? store.seedvr2Downloaded
321
+ : m.id === "ltx"
322
+ ? store.ltxDownloaded
323
+ : m.id === "ltx-base"
324
+ ? store.ltxBaseDownloaded
325
+ : m.id === "gemma"
326
+ ? store.gemmaDownloaded
327
+ : m.id === "dots-tts"
328
+ ? store.dotsTtsDownloaded
329
+ : store.ttsDownloaded;
309
330
  return (
310
331
  <ModelRow key={m.id} name={m.name} desc={m.desc}>
311
332
  <StatusBadge value={downloaded} />
@@ -0,0 +1,342 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useGenerationStore } from "../../stores/generationStore";
3
+ import { useProjectStore } from "../../stores/projectStore";
4
+ import { useQueueStore } from "../../stores/queueStore";
5
+ import { useAiModelStore } from "../../stores/aiModelStore";
6
+ import { useUpscaleStore, type UpscaleMode } from "../../stores/upscaleStore";
7
+ import TaskQueuePanel from "./TaskQueuePanel";
8
+ import TerminalLogPanel from "./TerminalLogPanel";
9
+
10
+ interface Props {
11
+ projectId: string;
12
+ }
13
+
14
+ export default function UpscaleTab({ projectId }: Props) {
15
+ const gen = useGenerationStore();
16
+ const { openFolder } = useProjectStore();
17
+ const queue = useQueueStore();
18
+ const ai = useAiModelStore();
19
+ const up = useUpscaleStore();
20
+
21
+ const fileInputRef = useRef<HTMLInputElement>(null);
22
+
23
+ useEffect(() => {
24
+ gen.fetchProjectImages(projectId);
25
+ ai.checkStatus();
26
+ // eslint-disable-next-line react-hooks/exhaustive-deps
27
+ }, [projectId]);
28
+
29
+ // Stream the generation queue so upscale tasks surface here.
30
+ useEffect(() => {
31
+ queue.startStreaming(projectId);
32
+ return () => queue.stopStreaming();
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ }, [projectId]);
35
+
36
+ // Reconcile upscale state with the latest queue task state.
37
+ useEffect(() => {
38
+ for (const task of queue.tasks) up.applyQueueTask(task);
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [queue.tasks, projectId]);
41
+
42
+ const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
43
+ const file = e.target.files?.[0];
44
+ e.target.value = "";
45
+ if (!file) return;
46
+ const reader = new FileReader();
47
+ reader.onload = async () => {
48
+ const uploadedPath = await gen.uploadImage(
49
+ projectId,
50
+ reader.result as string,
51
+ file.name,
52
+ );
53
+ if (uploadedPath) {
54
+ gen.fetchProjectImages(projectId);
55
+ const { uploadedImageFilename, uploadedImageUrl } =
56
+ useGenerationStore.getState();
57
+ up.setImage({
58
+ filename: uploadedImageFilename || file.name,
59
+ url: uploadedImageUrl || "",
60
+ });
61
+ }
62
+ };
63
+ reader.readAsDataURL(file);
64
+ };
65
+
66
+ const busy = up.generating || ai.downloading !== null;
67
+
68
+ // ========== SVG Icons ==========
69
+
70
+ const UpscaleIcon = (
71
+ <svg
72
+ width="18"
73
+ height="18"
74
+ viewBox="0 0 24 24"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ strokeWidth="2"
78
+ strokeLinecap="round"
79
+ strokeLinejoin="round"
80
+ >
81
+ <polyline points="15 3 21 3 21 9" />
82
+ <polyline points="9 21 3 21 3 15" />
83
+ <line x1="21" y1="3" x2="14" y2="10" />
84
+ <line x1="3" y1="21" x2="10" y2="14" />
85
+ </svg>
86
+ );
87
+
88
+ const DownloadIcon = (
89
+ <svg
90
+ width="16"
91
+ height="16"
92
+ viewBox="0 0 24 24"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ strokeWidth="2"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ >
99
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
100
+ <polyline points="7 10 12 15 17 10" />
101
+ <line x1="12" y1="15" x2="12" y2="3" />
102
+ </svg>
103
+ );
104
+
105
+ const SparkleIcon = (
106
+ <svg
107
+ width="16"
108
+ height="16"
109
+ viewBox="0 0 24 24"
110
+ fill="none"
111
+ stroke="currentColor"
112
+ strokeWidth="2"
113
+ >
114
+ <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" />
115
+ </svg>
116
+ );
117
+
118
+ const SpinnerIcon = (
119
+ <svg
120
+ className="animate-spin text-tiffany-600"
121
+ width="16"
122
+ height="16"
123
+ viewBox="0 0 24 24"
124
+ fill="none"
125
+ stroke="currentColor"
126
+ strokeWidth="2"
127
+ >
128
+ <circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
129
+ <path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
130
+ </svg>
131
+ );
132
+
133
+ const FolderIcon = (
134
+ <svg
135
+ width="16"
136
+ height="16"
137
+ viewBox="0 0 24 24"
138
+ fill="none"
139
+ stroke="currentColor"
140
+ strokeWidth="2"
141
+ strokeLinecap="round"
142
+ strokeLinejoin="round"
143
+ >
144
+ <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" />
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">{UpscaleIcon}</span>
152
+ <h2 className="text-base font-semibold text-ink-900">Upscale Media</h2>
153
+ <button
154
+ onClick={() => ai.downloadModel("seedvr2")}
155
+ disabled={ai.downloading !== null}
156
+ className="ml-auto flex items-center gap-2 px-4 py-2 text-xs font-medium rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300 disabled:opacity-50"
157
+ >
158
+ {ai.downloading === "seedvr2" ? SpinnerIcon : DownloadIcon}
159
+ {ai.downloading === "seedvr2"
160
+ ? "Downloading…"
161
+ : "Download SeedVR2 Model"}
162
+ </button>
163
+ </div>
164
+
165
+ <TaskQueuePanel projectId={projectId} />
166
+ <TerminalLogPanel />
167
+
168
+ {/* ===== Model status ===== */}
169
+ <div className="flex items-center gap-1.5 text-xs font-medium">
170
+ {ai.seedvr2Downloaded === null ? (
171
+ <span className="inline-flex items-center gap-1.5 text-ink-500">
172
+ {SpinnerIcon}
173
+ Checking…
174
+ </span>
175
+ ) : ai.seedvr2Downloaded ? (
176
+ <span className="text-emerald-600">✓ SeedVR2 model downloaded</span>
177
+ ) : (
178
+ <span className="text-amber-600">SeedVR2 model not downloaded</span>
179
+ )}
180
+ </div>
181
+
182
+ {/* ===== Upload ===== */}
183
+ <div>
184
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
185
+ Upload Image
186
+ </label>
187
+ <input
188
+ ref={fileInputRef}
189
+ type="file"
190
+ accept="image/*"
191
+ onChange={handleUpload}
192
+ disabled={gen.uploading || up.generating}
193
+ className="inline-block text-sm text-ink-700 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-sm file:font-medium file:bg-ink-100 file:text-ink-700 hover:file:bg-ink-200 file:cursor-pointer file:transition-colors disabled:opacity-50"
194
+ />
195
+ {gen.uploading && (
196
+ <p className="text-xs text-ink-600 mt-1">Uploading...</p>
197
+ )}
198
+ {gen.uploadError && (
199
+ <p className="text-xs text-red-600 mt-1">{gen.uploadError}</p>
200
+ )}
201
+ </div>
202
+
203
+ {/* ===== Pick image ===== */}
204
+ <div>
205
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
206
+ Pick Image
207
+ </label>
208
+ {gen.projectImagesLoading ? (
209
+ <p className="text-xs text-ink-500 italic py-4 text-center">
210
+ Loading images...
211
+ </p>
212
+ ) : gen.projectImages.length === 0 ? (
213
+ <p className="text-xs text-ink-500 italic py-4 text-center border border-dashed border-ink-200 rounded-2xl">
214
+ No images yet. Upload one above.
215
+ </p>
216
+ ) : (
217
+ <div className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 gap-2 p-1">
218
+ {gen.projectImages.map((img) => {
219
+ const isSelected = up.image?.filename === img.filename;
220
+ const fullUrl = img.url.startsWith("http")
221
+ ? img.url
222
+ : `http://localhost:${(window as any).PORT}${img.url}`;
223
+ return (
224
+ <button
225
+ key={`${img.source}-${img.filename}`}
226
+ onClick={() =>
227
+ up.setImage({ filename: img.filename, url: fullUrl })
228
+ }
229
+ disabled={up.generating}
230
+ className={`relative rounded-xl border-2 overflow-hidden transition-all ${
231
+ isSelected
232
+ ? "border-tiffany-500 ring-2 ring-tiffany-500/40"
233
+ : "border-ink-200 hover:border-ink-300"
234
+ } disabled:opacity-50`}
235
+ >
236
+ <img
237
+ src={fullUrl}
238
+ alt={img.filename}
239
+ className="aspect-square object-cover object-center w-full"
240
+ />
241
+ <span className="absolute bottom-0 left-0 right-0 bg-white/80 backdrop-blur-sm px-1.5 py-0.5 text-[10px] text-ink-700 truncate text-center">
242
+ {img.filename}
243
+ </span>
244
+ </button>
245
+ );
246
+ })}
247
+ </div>
248
+ )}
249
+ {up.image && (
250
+ <p className="text-xs text-ink-600/60 mt-1.5">
251
+ Selected:{" "}
252
+ <span className="font-medium text-ink-700">
253
+ {up.image.filename}
254
+ </span>
255
+ </p>
256
+ )}
257
+ </div>
258
+
259
+ {/* ===== Mode ===== */}
260
+ <div>
261
+ <label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
262
+ Mode
263
+ </label>
264
+ <div className="flex flex-wrap gap-2">
265
+ {[
266
+ { value: "1x" as UpscaleMode, label: "Refine (1x)" },
267
+ { value: "1000" as UpscaleMode, label: "Upscale to 1000px" },
268
+ { value: "1500" as UpscaleMode, label: "Upscale to 1500px" },
269
+ { value: "2000" as UpscaleMode, label: "Upscale to 2000px" },
270
+ { value: "2500" as UpscaleMode, label: "Upscale to 2500px" },
271
+ ].map((m) => (
272
+ <button
273
+ key={m.value}
274
+ onClick={() => up.setMode(m.value)}
275
+ disabled={up.generating}
276
+ className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
277
+ up.mode === m.value
278
+ ? "bg-ink-100 border-ink-300 text-ink-800"
279
+ : "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
280
+ } disabled:opacity-50`}
281
+ >
282
+ {m.label}
283
+ </button>
284
+ ))}
285
+ </div>
286
+ </div>
287
+
288
+ {/* ===== Generate ===== */}
289
+ {up.generating ? (
290
+ <div className="flex items-center gap-2 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
291
+ {SpinnerIcon}
292
+ <span className="text-sm font-medium text-ink-700">Upscaling...</span>
293
+ </div>
294
+ ) : (
295
+ <button
296
+ onClick={() => up.generate(projectId)}
297
+ disabled={busy || !up.image}
298
+ 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"
299
+ >
300
+ {SparkleIcon}
301
+ Upscale Image
302
+ </button>
303
+ )}
304
+
305
+ {/* ===== Open output folder ===== */}
306
+ <button
307
+ onClick={() => openFolder(projectId, "output")}
308
+ 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"
309
+ >
310
+ {FolderIcon}
311
+ Open Output Folder
312
+ </button>
313
+
314
+ {/* ===== Error ===== */}
315
+ {up.error && (
316
+ <div className="p-5 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
317
+ {up.error}
318
+ </div>
319
+ )}
320
+
321
+ {/* ===== Result ===== */}
322
+ {up.result && (
323
+ <div>
324
+ <div className="flex items-center justify-between mb-2">
325
+ <label className="text-xs font-semibold text-ink-700 uppercase tracking-wider">
326
+ Upscaled Image
327
+ </label>
328
+ <button
329
+ onClick={() => up.clearResult()}
330
+ className="px-3 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors"
331
+ >
332
+ Remove
333
+ </button>
334
+ </div>
335
+ <div className="rounded-2xl overflow-hidden border border-ink-200 shadow-card inline-block">
336
+ <img src={up.result} alt="Upscaled" className="max-w-full h-auto" />
337
+ </div>
338
+ </div>
339
+ )}
340
+ </div>
341
+ );
342
+ }