@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
2
|
import Mustache from "mustache";
|
|
3
3
|
import Papa from "papaparse";
|
|
4
|
+
import type { QueueTask } from "./queueStore";
|
|
4
5
|
import {
|
|
5
6
|
loadTextToImageState,
|
|
6
7
|
saveTextToImageState,
|
|
@@ -15,17 +16,17 @@ export type GenerationTab =
|
|
|
15
16
|
| "movieStudio"
|
|
16
17
|
| "fastImageEdit"
|
|
17
18
|
| "video"
|
|
18
|
-
| "extend"
|
|
19
19
|
| "agent"
|
|
20
20
|
| "storyWriter"
|
|
21
|
-
| "extract"
|
|
22
|
-
| "sceneVisual"
|
|
23
21
|
| "textToImage"
|
|
24
22
|
| "batchVideo"
|
|
25
23
|
| "batchImageToVideo"
|
|
26
|
-
| "batchVoice"
|
|
27
24
|
| "llmServer"
|
|
28
|
-
| "aiModels"
|
|
25
|
+
| "aiModels"
|
|
26
|
+
| "upscale"
|
|
27
|
+
| "voiceClone"
|
|
28
|
+
| "audioToVideo"
|
|
29
|
+
| "advancedVoiceClone";
|
|
29
30
|
export type AspectRatio = "1:1" | "16:9" | "9:16" | "4:3" | "3:4";
|
|
30
31
|
export type Resolution =
|
|
31
32
|
| "320p"
|
|
@@ -76,16 +77,9 @@ interface VideoState {
|
|
|
76
77
|
aspectRatio: AspectRatio;
|
|
77
78
|
resolution: Resolution;
|
|
78
79
|
mode: VideoMode;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
logs: string[];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface ExtendState {
|
|
86
|
-
prompt: string;
|
|
87
|
-
extendDuration: number;
|
|
88
|
-
extendFrames: number;
|
|
80
|
+
model: string;
|
|
81
|
+
stage1Steps: number;
|
|
82
|
+
stage2Steps: number;
|
|
89
83
|
generating: boolean;
|
|
90
84
|
result: string | null;
|
|
91
85
|
error: string | null;
|
|
@@ -113,6 +107,8 @@ interface TextToImageState {
|
|
|
113
107
|
|
|
114
108
|
interface FastImageEditState {
|
|
115
109
|
prompt: string;
|
|
110
|
+
steps: number;
|
|
111
|
+
upscale: string;
|
|
116
112
|
referenceImages: ProjectImage[];
|
|
117
113
|
downloading: boolean;
|
|
118
114
|
downloadingLogs: string[];
|
|
@@ -158,18 +154,14 @@ interface GenerationStore {
|
|
|
158
154
|
setVideoAspectRatio: (v: AspectRatio) => void;
|
|
159
155
|
setVideoResolution: (v: Resolution) => void;
|
|
160
156
|
setVideoMode: (v: VideoMode) => void;
|
|
157
|
+
setVideoModel: (v: string) => void;
|
|
158
|
+
setVideoStage1Steps: (v: number) => void;
|
|
159
|
+
setVideoStage2Steps: (v: number) => void;
|
|
161
160
|
clearVideoResult: () => void;
|
|
162
161
|
generateVideo: (projectId: string, imagePath?: string) => Promise<void>;
|
|
162
|
+
applyVideoQueueTask: (task: QueueTask) => void;
|
|
163
163
|
cancelGenerate: () => void;
|
|
164
164
|
|
|
165
|
-
// Video extension
|
|
166
|
-
extend: ExtendState;
|
|
167
|
-
setExtendPrompt: (v: string) => void;
|
|
168
|
-
setExtendDuration: (v: number) => void;
|
|
169
|
-
setExtendFrames: (v: number) => void;
|
|
170
|
-
clearExtendResult: () => void;
|
|
171
|
-
generateExtend: (projectId: string, videoPath: string) => Promise<void>;
|
|
172
|
-
|
|
173
165
|
// Text-to-image (mlx-gen z-image-turbo)
|
|
174
166
|
textToImage: TextToImageState;
|
|
175
167
|
setTextToImagePrompt: (v: string) => void;
|
|
@@ -192,12 +184,15 @@ interface GenerationStore {
|
|
|
192
184
|
// Fast image edit (mlx-gen FLUX.2 Klein)
|
|
193
185
|
fastImageEdit: FastImageEditState;
|
|
194
186
|
setFastImageEditPrompt: (v: string) => void;
|
|
187
|
+
setFastImageEditSteps: (v: number) => void;
|
|
188
|
+
setFastImageEditUpscale: (v: string) => void;
|
|
195
189
|
toggleFastImageEditImage: (img: ProjectImage) => void;
|
|
196
190
|
clearFastImageEditImages: () => void;
|
|
197
191
|
clearFastImageEditResult: () => void;
|
|
198
192
|
checkFastImageEditStatus: () => Promise<void>;
|
|
199
193
|
downloadFastImageEditModel: () => Promise<void>;
|
|
200
194
|
generateFastImageEdit: (projectId: string) => Promise<void>;
|
|
195
|
+
applyFastImageEditQueueTask: (task: QueueTask) => void;
|
|
201
196
|
|
|
202
197
|
// Agent (mlx-vlm)
|
|
203
198
|
agent: AgentState;
|
|
@@ -214,8 +209,6 @@ interface GenerationStore {
|
|
|
214
209
|
projectVideos: ProjectVideo[];
|
|
215
210
|
projectVideosLoading: boolean;
|
|
216
211
|
fetchProjectVideos: (projectId: string) => Promise<void>;
|
|
217
|
-
selectedVideo: ProjectVideo | null;
|
|
218
|
-
selectVideo: (video: ProjectVideo | null) => void;
|
|
219
212
|
|
|
220
213
|
// Project audio picker
|
|
221
214
|
projectAudios: ProjectAudio[];
|
|
@@ -241,11 +234,6 @@ interface GenerationStore {
|
|
|
241
234
|
selectedImage: ProjectImage | null;
|
|
242
235
|
selectImage: (img: ProjectImage | null) => void;
|
|
243
236
|
|
|
244
|
-
// Character sheet picker
|
|
245
|
-
characterSheets: ProjectImage[];
|
|
246
|
-
characterSheetsLoading: boolean;
|
|
247
|
-
fetchCharacterSheets: (projectId: string) => Promise<void>;
|
|
248
|
-
|
|
249
237
|
// CSV batch generation
|
|
250
238
|
csvRows: Record<string, string>[];
|
|
251
239
|
csvColumns: string[];
|
|
@@ -270,7 +258,7 @@ interface GenerationStore {
|
|
|
270
258
|
export interface ProjectImage {
|
|
271
259
|
filename: string;
|
|
272
260
|
url: string;
|
|
273
|
-
source: "upload" | "generated"
|
|
261
|
+
source: "upload" | "generated";
|
|
274
262
|
}
|
|
275
263
|
|
|
276
264
|
export interface ProjectVideo {
|
|
@@ -391,42 +379,70 @@ async function resizeImageToPng(
|
|
|
391
379
|
}
|
|
392
380
|
}
|
|
393
381
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
): Promise<{ ok: boolean; error?: string;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
382
|
+
/** Enqueue a fast image edit task in the backend queue worker. */
|
|
383
|
+
async function enqueueFastImageEditTask(
|
|
384
|
+
projectId: string,
|
|
385
|
+
prompt: string,
|
|
386
|
+
images: string[],
|
|
387
|
+
steps: number,
|
|
388
|
+
upscale: string,
|
|
389
|
+
): Promise<{ ok: boolean; error?: string; taskId?: string }> {
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch(`${API_BASE}/api/queue/enqueue`, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: { "Content-Type": "application/json" },
|
|
394
|
+
body: JSON.stringify({
|
|
395
|
+
projectId,
|
|
396
|
+
type: "fast-image-edit",
|
|
397
|
+
label: "Fast image edit",
|
|
398
|
+
payload: { prompt, images, steps, upscale, projectId },
|
|
399
|
+
}),
|
|
400
|
+
});
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
return { ok: false, error: await res.text() };
|
|
403
|
+
}
|
|
404
|
+
const task = (await res.json()) as { id?: string };
|
|
405
|
+
return { ok: true, taskId: task.id };
|
|
406
|
+
} catch (e) {
|
|
407
|
+
return { ok: false, error: String(e) };
|
|
410
408
|
}
|
|
409
|
+
}
|
|
411
410
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
411
|
+
/** Enqueue a scene video generation task in the backend queue worker. */
|
|
412
|
+
async function enqueueImageToVideoTask(
|
|
413
|
+
projectId: string,
|
|
414
|
+
payload: {
|
|
415
|
+
prompt: string;
|
|
416
|
+
imagePath: string;
|
|
417
|
+
width: number;
|
|
418
|
+
height: number;
|
|
419
|
+
frames: number;
|
|
420
|
+
frameRate: number;
|
|
421
|
+
mode: string;
|
|
422
|
+
model: string;
|
|
423
|
+
stage1Steps: number;
|
|
424
|
+
stage2Steps: number;
|
|
425
|
+
},
|
|
426
|
+
): Promise<{ ok: boolean; error?: string; taskId?: string }> {
|
|
427
|
+
try {
|
|
428
|
+
const res = await fetch(`${API_BASE}/api/queue/enqueue`, {
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers: { "Content-Type": "application/json" },
|
|
431
|
+
body: JSON.stringify({
|
|
432
|
+
projectId,
|
|
433
|
+
type: "image-to-video",
|
|
434
|
+
label: "Scene video",
|
|
435
|
+
payload,
|
|
436
|
+
}),
|
|
437
|
+
});
|
|
438
|
+
if (!res.ok) {
|
|
439
|
+
return { ok: false, error: await res.text() };
|
|
426
440
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
441
|
+
const task = (await res.json()) as { id?: string };
|
|
442
|
+
return { ok: true, taskId: task.id };
|
|
443
|
+
} catch (e) {
|
|
444
|
+
return { ok: false, error: String(e) };
|
|
445
|
+
}
|
|
430
446
|
}
|
|
431
447
|
|
|
432
448
|
// ========== Abort Controllers ==========
|
|
@@ -434,6 +450,22 @@ async function requestFastImageEdit(
|
|
|
434
450
|
let generateAbortController: AbortController | null = null;
|
|
435
451
|
let batchAbortController: AbortController | null = null;
|
|
436
452
|
|
|
453
|
+
// ========== Fast Image Edit Queue ==========
|
|
454
|
+
|
|
455
|
+
/** Project whose fast-image-edit task should refresh the image grid on completion. */
|
|
456
|
+
let fastImageEditProjectId: string | null = null;
|
|
457
|
+
|
|
458
|
+
/** The queue task id enqueued by the current generate action, if any. */
|
|
459
|
+
let fastImageEditActiveTaskId: string | null = null;
|
|
460
|
+
|
|
461
|
+
// ========== Scene Video Queue ==========
|
|
462
|
+
|
|
463
|
+
/** Project whose image-to-video task should refresh the video grid on completion. */
|
|
464
|
+
let videoProjectId: string | null = null;
|
|
465
|
+
|
|
466
|
+
/** The queue task id enqueued by the current generate video action, if any. */
|
|
467
|
+
let videoActiveTaskId: string | null = null;
|
|
468
|
+
|
|
437
469
|
// ========== Beep ==========
|
|
438
470
|
|
|
439
471
|
function playBeep() {
|
|
@@ -474,16 +506,9 @@ const initialVideo: VideoState = {
|
|
|
474
506
|
aspectRatio: "1:1",
|
|
475
507
|
resolution: "480p",
|
|
476
508
|
mode: "distilled",
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
logs: [],
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
const initialExtend: ExtendState = {
|
|
484
|
-
prompt: "Continue the scene: the camera holds, motion flows naturally...",
|
|
485
|
-
extendDuration: 2,
|
|
486
|
-
extendFrames: 2 * 24 + 1,
|
|
509
|
+
model: "dgrauet/ltx-2.3-mlx-q8",
|
|
510
|
+
stage1Steps: 30,
|
|
511
|
+
stage2Steps: 3,
|
|
487
512
|
generating: false,
|
|
488
513
|
result: null,
|
|
489
514
|
error: null,
|
|
@@ -536,6 +561,8 @@ const initialTextToImage: TextToImageState = {
|
|
|
536
561
|
|
|
537
562
|
const initialFastImageEdit: FastImageEditState = {
|
|
538
563
|
prompt: "",
|
|
564
|
+
steps: 4,
|
|
565
|
+
upscale: "none",
|
|
539
566
|
referenceImages: [],
|
|
540
567
|
downloading: false,
|
|
541
568
|
downloadingLogs: [],
|
|
@@ -692,6 +719,21 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
692
719
|
setVideoResolution: (resolution) =>
|
|
693
720
|
set((s) => ({ video: { ...s.video, resolution } })),
|
|
694
721
|
setVideoMode: (mode) => set((s) => ({ video: { ...s.video, mode } })),
|
|
722
|
+
setVideoModel: (model) => set((s) => ({ video: { ...s.video, model } })),
|
|
723
|
+
setVideoStage1Steps: (stage1Steps) =>
|
|
724
|
+
set((s) => ({
|
|
725
|
+
video: {
|
|
726
|
+
...s.video,
|
|
727
|
+
stage1Steps: Math.max(1, Math.round(Number(stage1Steps)) || 1),
|
|
728
|
+
},
|
|
729
|
+
})),
|
|
730
|
+
setVideoStage2Steps: (stage2Steps) =>
|
|
731
|
+
set((s) => ({
|
|
732
|
+
video: {
|
|
733
|
+
...s.video,
|
|
734
|
+
stage2Steps: Math.max(1, Math.round(Number(stage2Steps)) || 1),
|
|
735
|
+
},
|
|
736
|
+
})),
|
|
695
737
|
clearVideoResult: () =>
|
|
696
738
|
set((s) => ({
|
|
697
739
|
video: { ...s.video, result: null, error: null, logs: [] },
|
|
@@ -741,10 +783,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
741
783
|
// The backend now only accepts bare filenames — extract just the basename
|
|
742
784
|
resolvedImagePath = resolvedImagePath.split("/").pop() || resolvedImagePath;
|
|
743
785
|
|
|
744
|
-
// Create a fresh AbortController for this single generate run
|
|
745
|
-
generateAbortController = new AbortController();
|
|
746
|
-
const signal = generateAbortController.signal;
|
|
747
|
-
|
|
748
786
|
set((s) => ({
|
|
749
787
|
video: {
|
|
750
788
|
...s.video,
|
|
@@ -755,75 +793,63 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
755
793
|
},
|
|
756
794
|
}));
|
|
757
795
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
796
|
+
videoProjectId = projectId;
|
|
797
|
+
const r = await enqueueImageToVideoTask(projectId, {
|
|
798
|
+
prompt: video.prompt.trim(),
|
|
799
|
+
imagePath: resolvedImagePath,
|
|
800
|
+
width,
|
|
801
|
+
height,
|
|
802
|
+
frames: video.duration * 24 + 1,
|
|
803
|
+
frameRate: 24,
|
|
804
|
+
mode: video.mode,
|
|
805
|
+
model: video.model,
|
|
806
|
+
stage1Steps: video.stage1Steps,
|
|
807
|
+
stage2Steps: video.stage2Steps,
|
|
808
|
+
});
|
|
809
|
+
videoActiveTaskId = r.taskId ?? null;
|
|
810
|
+
if (!r.ok) {
|
|
811
|
+
set((s) => ({
|
|
812
|
+
video: {
|
|
813
|
+
...s.video,
|
|
814
|
+
generating: false,
|
|
815
|
+
error: r.error ?? "Failed to enqueue video",
|
|
816
|
+
},
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
},
|
|
774
820
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
821
|
+
// Reconcile the scene video state with the latest queue task state. Only the
|
|
822
|
+
// task enqueued by this tab is reflected, so past tasks never surface stale
|
|
823
|
+
// results.
|
|
824
|
+
applyVideoQueueTask: (task) => {
|
|
825
|
+
if (task.type !== "image-to-video") return;
|
|
826
|
+
if (task.id !== videoActiveTaskId) return;
|
|
827
|
+
const v = get().video;
|
|
783
828
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
break;
|
|
794
|
-
case "complete":
|
|
795
|
-
set((s) => ({
|
|
796
|
-
video: {
|
|
797
|
-
...s.video,
|
|
798
|
-
generating: false,
|
|
799
|
-
result: `http://localhost:${(window as any).PORT}/api/files?path=${encodeURIComponent(data.path)}`,
|
|
800
|
-
},
|
|
801
|
-
}));
|
|
802
|
-
break;
|
|
803
|
-
case "error":
|
|
804
|
-
set((s) => ({
|
|
805
|
-
video: {
|
|
806
|
-
...s.video,
|
|
807
|
-
generating: false,
|
|
808
|
-
error: data.error || "Video generation failed",
|
|
809
|
-
},
|
|
810
|
-
}));
|
|
811
|
-
break;
|
|
812
|
-
}
|
|
829
|
+
if (task.status === "completed") {
|
|
830
|
+
const url = task.result?.url;
|
|
831
|
+
set({
|
|
832
|
+
video: {
|
|
833
|
+
...v,
|
|
834
|
+
generating: false,
|
|
835
|
+
result: url ? resolveImageUrl(url) : null,
|
|
836
|
+
error: null,
|
|
837
|
+
},
|
|
813
838
|
});
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
839
|
+
if (videoProjectId) get().fetchProjectVideos(videoProjectId);
|
|
840
|
+
} else if (task.status === "failed") {
|
|
841
|
+
set({
|
|
842
|
+
video: {
|
|
843
|
+
...v,
|
|
844
|
+
generating: false,
|
|
845
|
+
error: task.error ?? "Video generation failed",
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
} else if (task.status === "cancelled" || task.status === "paused") {
|
|
849
|
+
set({ video: { ...v, generating: false, error: null } });
|
|
850
|
+
} else {
|
|
851
|
+
// pending / running
|
|
852
|
+
set({ video: { ...v, generating: true, error: null } });
|
|
827
853
|
}
|
|
828
854
|
},
|
|
829
855
|
|
|
@@ -892,31 +918,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
892
918
|
}
|
|
893
919
|
},
|
|
894
920
|
|
|
895
|
-
// ---- Character Sheets ----
|
|
896
|
-
characterSheets: [],
|
|
897
|
-
characterSheetsLoading: false,
|
|
898
|
-
|
|
899
|
-
fetchCharacterSheets: async (projectId) => {
|
|
900
|
-
set({ characterSheetsLoading: true });
|
|
901
|
-
try {
|
|
902
|
-
const res = await fetch(
|
|
903
|
-
`${API_BASE}/api/projects/${projectId}/character-sheets`,
|
|
904
|
-
);
|
|
905
|
-
if (!res.ok) throw new Error(await res.text());
|
|
906
|
-
const sheets: { filename: string; url: string }[] = await res.json();
|
|
907
|
-
const resolved: ProjectImage[] = sheets.map((sheet) => ({
|
|
908
|
-
filename: sheet.filename,
|
|
909
|
-
url: sheet.url.startsWith("http")
|
|
910
|
-
? sheet.url
|
|
911
|
-
: `http://localhost:${(window as any).PORT}${sheet.url}`,
|
|
912
|
-
source: "characterSheet",
|
|
913
|
-
}));
|
|
914
|
-
set({ characterSheets: resolved, characterSheetsLoading: false });
|
|
915
|
-
} catch {
|
|
916
|
-
set({ characterSheetsLoading: false });
|
|
917
|
-
}
|
|
918
|
-
},
|
|
919
|
-
|
|
920
921
|
// ---- CSV Batch ----
|
|
921
922
|
csvRows: [],
|
|
922
923
|
csvColumns: [],
|
|
@@ -1076,6 +1077,9 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
1076
1077
|
frames: video.duration * 24 + 1,
|
|
1077
1078
|
frameRate: 24,
|
|
1078
1079
|
mode: video.mode,
|
|
1080
|
+
model: video.model,
|
|
1081
|
+
stage1Steps: video.stage1Steps,
|
|
1082
|
+
stage2Steps: video.stage2Steps,
|
|
1079
1083
|
}),
|
|
1080
1084
|
signal,
|
|
1081
1085
|
});
|
|
@@ -1143,140 +1147,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
1143
1147
|
}
|
|
1144
1148
|
},
|
|
1145
1149
|
|
|
1146
|
-
// ---- Video Extension ----
|
|
1147
|
-
extend: { ...initialExtend },
|
|
1148
|
-
|
|
1149
|
-
setExtendPrompt: (prompt) =>
|
|
1150
|
-
set((s) => ({ extend: { ...s.extend, prompt, error: null } })),
|
|
1151
|
-
setExtendDuration: (extendDuration) =>
|
|
1152
|
-
set((s) => ({
|
|
1153
|
-
extend: {
|
|
1154
|
-
...s.extend,
|
|
1155
|
-
extendDuration,
|
|
1156
|
-
extendFrames: extendDuration * 24 + 1,
|
|
1157
|
-
},
|
|
1158
|
-
})),
|
|
1159
|
-
setExtendFrames: (extendFrames) =>
|
|
1160
|
-
set((s) => ({
|
|
1161
|
-
extend: {
|
|
1162
|
-
...s.extend,
|
|
1163
|
-
extendFrames,
|
|
1164
|
-
extendDuration: Math.max(0.5, (extendFrames - 1) / 24),
|
|
1165
|
-
},
|
|
1166
|
-
})),
|
|
1167
|
-
clearExtendResult: () =>
|
|
1168
|
-
set((s) => ({
|
|
1169
|
-
extend: { ...s.extend, result: null, error: null, logs: [] },
|
|
1170
|
-
})),
|
|
1171
|
-
|
|
1172
|
-
generateExtend: async (projectId, videoPath) => {
|
|
1173
|
-
const { extend } = get();
|
|
1174
|
-
if (!extend.prompt.trim() || extend.generating) return;
|
|
1175
|
-
|
|
1176
|
-
// Extract raw file path from HTTP URL (e.g. http://localhost:PORT/api/files?path=...)
|
|
1177
|
-
let resolvedVideoPath = videoPath || "";
|
|
1178
|
-
if (!resolvedVideoPath) {
|
|
1179
|
-
set((s) => ({
|
|
1180
|
-
extend: { ...s.extend, error: "Select a video to extend first." },
|
|
1181
|
-
}));
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
if (resolvedVideoPath.includes("/api/files?path=")) {
|
|
1185
|
-
try {
|
|
1186
|
-
const url = new URL(resolvedVideoPath);
|
|
1187
|
-
resolvedVideoPath = url.searchParams.get("path") || resolvedVideoPath;
|
|
1188
|
-
} catch {
|
|
1189
|
-
// not a valid URL, use as-is
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
if (resolvedVideoPath.startsWith("file://")) {
|
|
1193
|
-
resolvedVideoPath = resolvedVideoPath.slice(7);
|
|
1194
|
-
}
|
|
1195
|
-
// The backend only accepts bare filenames — extract just the basename
|
|
1196
|
-
resolvedVideoPath = resolvedVideoPath.split("/").pop() || resolvedVideoPath;
|
|
1197
|
-
|
|
1198
|
-
// Create a fresh AbortController for this extend run
|
|
1199
|
-
generateAbortController = new AbortController();
|
|
1200
|
-
const signal = generateAbortController.signal;
|
|
1201
|
-
|
|
1202
|
-
set((s) => ({
|
|
1203
|
-
extend: {
|
|
1204
|
-
...s.extend,
|
|
1205
|
-
generating: true,
|
|
1206
|
-
error: null,
|
|
1207
|
-
result: null,
|
|
1208
|
-
logs: [],
|
|
1209
|
-
},
|
|
1210
|
-
}));
|
|
1211
|
-
|
|
1212
|
-
try {
|
|
1213
|
-
const res = await fetch(`${API_BASE}/api/render/extend-video`, {
|
|
1214
|
-
method: "POST",
|
|
1215
|
-
headers: { "Content-Type": "application/json" },
|
|
1216
|
-
body: JSON.stringify({
|
|
1217
|
-
prompt: extend.prompt.trim(),
|
|
1218
|
-
videoPath: resolvedVideoPath,
|
|
1219
|
-
projectId,
|
|
1220
|
-
extendFrames: extend.extendDuration * 24 + 1,
|
|
1221
|
-
}),
|
|
1222
|
-
signal,
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
if (!res.ok) {
|
|
1226
|
-
const err = await res.text();
|
|
1227
|
-
set((s) => ({
|
|
1228
|
-
extend: { ...s.extend, generating: false, error: err },
|
|
1229
|
-
}));
|
|
1230
|
-
generateAbortController = null;
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
await readSSEStream(res, (event, data) => {
|
|
1235
|
-
switch (event) {
|
|
1236
|
-
case "log":
|
|
1237
|
-
set((s) => ({
|
|
1238
|
-
extend: {
|
|
1239
|
-
...s.extend,
|
|
1240
|
-
logs: [...s.extend.logs, data.text as string],
|
|
1241
|
-
},
|
|
1242
|
-
}));
|
|
1243
|
-
break;
|
|
1244
|
-
case "complete":
|
|
1245
|
-
set((s) => ({
|
|
1246
|
-
extend: {
|
|
1247
|
-
...s.extend,
|
|
1248
|
-
generating: false,
|
|
1249
|
-
result: `http://localhost:${(window as any).PORT}/api/files?path=${encodeURIComponent(data.path)}`,
|
|
1250
|
-
},
|
|
1251
|
-
}));
|
|
1252
|
-
// Refresh the video grid so the new extended-*.mp4 appears
|
|
1253
|
-
get().fetchProjectVideos(projectId);
|
|
1254
|
-
break;
|
|
1255
|
-
case "error":
|
|
1256
|
-
set((s) => ({
|
|
1257
|
-
extend: {
|
|
1258
|
-
...s.extend,
|
|
1259
|
-
generating: false,
|
|
1260
|
-
error: data.error || "Video extension failed",
|
|
1261
|
-
},
|
|
1262
|
-
}));
|
|
1263
|
-
break;
|
|
1264
|
-
}
|
|
1265
|
-
});
|
|
1266
|
-
} catch (e: any) {
|
|
1267
|
-
// If the request was aborted, just stop silently
|
|
1268
|
-
if (e?.name !== "AbortError") {
|
|
1269
|
-
set((s) => ({
|
|
1270
|
-
extend: { ...s.extend, generating: false, error: String(e) },
|
|
1271
|
-
}));
|
|
1272
|
-
} else {
|
|
1273
|
-
set((s) => ({ extend: { ...s.extend, generating: false } }));
|
|
1274
|
-
}
|
|
1275
|
-
} finally {
|
|
1276
|
-
generateAbortController = null;
|
|
1277
|
-
}
|
|
1278
|
-
},
|
|
1279
|
-
|
|
1280
1150
|
// ---- Fast Image Edit (FLUX.2 Klein) ----
|
|
1281
1151
|
fastImageEdit: { ...initialFastImageEdit },
|
|
1282
1152
|
|
|
@@ -1285,6 +1155,17 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
1285
1155
|
fastImageEdit: { ...s.fastImageEdit, prompt, error: null },
|
|
1286
1156
|
})),
|
|
1287
1157
|
|
|
1158
|
+
setFastImageEditSteps: (steps) =>
|
|
1159
|
+
set((s) => ({
|
|
1160
|
+
fastImageEdit: {
|
|
1161
|
+
...s.fastImageEdit,
|
|
1162
|
+
steps: Math.max(1, Math.round(Number(steps)) || 1),
|
|
1163
|
+
},
|
|
1164
|
+
})),
|
|
1165
|
+
|
|
1166
|
+
setFastImageEditUpscale: (upscale) =>
|
|
1167
|
+
set((s) => ({ fastImageEdit: { ...s.fastImageEdit, upscale } })),
|
|
1168
|
+
|
|
1288
1169
|
toggleFastImageEditImage: (img) =>
|
|
1289
1170
|
set((s) => {
|
|
1290
1171
|
const current = s.fastImageEdit.referenceImages;
|
|
@@ -1446,32 +1327,68 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
1446
1327
|
images.push(processed.dataUrl);
|
|
1447
1328
|
}
|
|
1448
1329
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
set((s) => ({
|
|
1457
|
-
fastImageEdit: {
|
|
1458
|
-
...s.fastImageEdit,
|
|
1459
|
-
logs: [...s.fastImageEdit.logs, text],
|
|
1460
|
-
},
|
|
1461
|
-
})),
|
|
1330
|
+
fastImageEditProjectId = projectId;
|
|
1331
|
+
const r = await enqueueFastImageEditTask(
|
|
1332
|
+
projectId,
|
|
1333
|
+
fastImageEdit.prompt.trim(),
|
|
1334
|
+
images,
|
|
1335
|
+
fastImageEdit.steps,
|
|
1336
|
+
fastImageEdit.upscale,
|
|
1462
1337
|
);
|
|
1338
|
+
fastImageEditActiveTaskId = r.taskId ?? null;
|
|
1339
|
+
if (!r.ok) {
|
|
1340
|
+
set((s) => ({
|
|
1341
|
+
fastImageEdit: {
|
|
1342
|
+
...s.fastImageEdit,
|
|
1343
|
+
generating: false,
|
|
1344
|
+
error: r.error ?? "Failed to enqueue fast image edit",
|
|
1345
|
+
},
|
|
1346
|
+
}));
|
|
1347
|
+
}
|
|
1348
|
+
},
|
|
1463
1349
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}));
|
|
1350
|
+
// Reconcile the fast image edit state with the latest queue task state.
|
|
1351
|
+
// Only the task enqueued by this tab is reflected, so tasks finished before
|
|
1352
|
+
// the tab opened never surface stale results.
|
|
1353
|
+
applyFastImageEditQueueTask: (task) => {
|
|
1354
|
+
if (task.type !== "fast-image-edit") return;
|
|
1355
|
+
if (task.id !== fastImageEditActiveTaskId) return;
|
|
1356
|
+
const fie = get().fastImageEdit;
|
|
1472
1357
|
|
|
1473
|
-
if (
|
|
1474
|
-
|
|
1358
|
+
if (task.status === "completed") {
|
|
1359
|
+
const url = task.result?.url;
|
|
1360
|
+
set({
|
|
1361
|
+
fastImageEdit: {
|
|
1362
|
+
...fie,
|
|
1363
|
+
generating: false,
|
|
1364
|
+
result: url ? resolveImageUrl(url) : null,
|
|
1365
|
+
error: null,
|
|
1366
|
+
},
|
|
1367
|
+
});
|
|
1368
|
+
if (fastImageEditProjectId) {
|
|
1369
|
+
get().fetchProjectImages(fastImageEditProjectId);
|
|
1370
|
+
}
|
|
1371
|
+
} else if (
|
|
1372
|
+
task.status === "failed" ||
|
|
1373
|
+
task.status === "cancelled" ||
|
|
1374
|
+
task.status === "paused"
|
|
1375
|
+
) {
|
|
1376
|
+
set({
|
|
1377
|
+
fastImageEdit: {
|
|
1378
|
+
...fie,
|
|
1379
|
+
generating: false,
|
|
1380
|
+
error: task.error ?? "Fast image edit failed",
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
} else {
|
|
1384
|
+
// pending / running
|
|
1385
|
+
set({
|
|
1386
|
+
fastImageEdit: {
|
|
1387
|
+
...fie,
|
|
1388
|
+
generating: true,
|
|
1389
|
+
error: null,
|
|
1390
|
+
},
|
|
1391
|
+
});
|
|
1475
1392
|
}
|
|
1476
1393
|
},
|
|
1477
1394
|
|
|
@@ -2003,7 +1920,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
2003
1920
|
// ---- Project Videos ----
|
|
2004
1921
|
projectVideos: [],
|
|
2005
1922
|
projectVideosLoading: false,
|
|
2006
|
-
selectedVideo: null,
|
|
2007
1923
|
|
|
2008
1924
|
fetchProjectVideos: async (projectId) => {
|
|
2009
1925
|
set({ projectVideosLoading: true });
|
|
@@ -2024,17 +1940,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
2024
1940
|
}
|
|
2025
1941
|
},
|
|
2026
1942
|
|
|
2027
|
-
selectVideo: (video) => {
|
|
2028
|
-
if (!video) {
|
|
2029
|
-
set({ selectedVideo: null });
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
const fullUrl = video.url.startsWith("http")
|
|
2033
|
-
? video.url
|
|
2034
|
-
: `http://localhost:${(window as any).PORT}${video.url}`;
|
|
2035
|
-
set({ selectedVideo: { ...video, url: fullUrl } });
|
|
2036
|
-
},
|
|
2037
|
-
|
|
2038
1943
|
// ---- Project Audio ----
|
|
2039
1944
|
projectAudios: [],
|
|
2040
1945
|
projectAudiosLoading: false,
|
|
@@ -2065,7 +1970,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
2065
1970
|
// Immediately reset generating state so UI returns to ready
|
|
2066
1971
|
set((s) => ({
|
|
2067
1972
|
video: { ...s.video, generating: false },
|
|
2068
|
-
extend: { ...s.extend, generating: false },
|
|
2069
1973
|
textToImage: { ...s.textToImage, generating: false },
|
|
2070
1974
|
}));
|
|
2071
1975
|
// Also kill the backend spawn process
|
|
@@ -2103,12 +2007,15 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
2103
2007
|
},
|
|
2104
2008
|
|
|
2105
2009
|
// ---- Reset ----
|
|
2106
|
-
resetAll: () =>
|
|
2010
|
+
resetAll: () => {
|
|
2011
|
+
fastImageEditProjectId = null;
|
|
2012
|
+
fastImageEditActiveTaskId = null;
|
|
2013
|
+
videoProjectId = null;
|
|
2014
|
+
videoActiveTaskId = null;
|
|
2107
2015
|
set({
|
|
2108
2016
|
activeTab: "movieStudio",
|
|
2109
2017
|
image: { ...initialImage },
|
|
2110
2018
|
video: { ...initialVideo },
|
|
2111
|
-
extend: { ...initialExtend },
|
|
2112
2019
|
fastImageEdit: { ...initialFastImageEdit },
|
|
2113
2020
|
textToImage: { ...initialTextToImage },
|
|
2114
2021
|
textToImageProjectId: null,
|
|
@@ -2120,12 +2027,9 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
2120
2027
|
uploadedImageFilename: null,
|
|
2121
2028
|
uploadedImagePath: null,
|
|
2122
2029
|
projectImages: [],
|
|
2123
|
-
characterSheets: [],
|
|
2124
|
-
characterSheetsLoading: false,
|
|
2125
2030
|
selectedImage: null,
|
|
2126
2031
|
projectVideos: [],
|
|
2127
2032
|
projectVideosLoading: false,
|
|
2128
|
-
selectedVideo: null,
|
|
2129
2033
|
projectAudios: [],
|
|
2130
2034
|
projectAudiosLoading: false,
|
|
2131
2035
|
csvRows: [],
|
|
@@ -2135,5 +2039,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
|
|
|
2135
2039
|
batchRunning: false,
|
|
2136
2040
|
batchProgress: null,
|
|
2137
2041
|
batchCancelRequested: false,
|
|
2138
|
-
})
|
|
2042
|
+
});
|
|
2043
|
+
},
|
|
2139
2044
|
}));
|