@effectnode/media 0.5.0 → 0.7.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/LICENSE +21 -0
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
- package/dist/backend/movie-backend/agent/agent-backend.js +172 -83
- package/dist/backend/movie-backend/agent/tools/index.js +0 -2
- package/dist/backend/movie-backend/core.js +1 -1
- package/dist/backend/movie-backend/generation-queue.d.ts +1 -1
- package/dist/backend/movie-backend/generation-queue.js +128 -9
- package/dist/backend/movie-backend/render-media.js +49 -226
- package/frontend/index.html +3 -5
- package/frontend/src/movie-app/SetupPage.tsx +1 -1
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +127 -23
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +38 -37
- package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +37 -9
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +0 -33
- package/frontend/src/movie-app/components/ProjectManager.tsx +1 -1
- package/frontend/src/movie-app/index.css +46 -20
- package/frontend/src/movie-app/index.html +1 -1
- package/frontend/src/movie-app/stores/aiModelStore.ts +20 -7
- package/frontend/src/movie-app/stores/generationStore.ts +0 -1
- package/frontend/src/movie-app/stores/movieStudioStore.ts +359 -32
- package/frontend/src/movie-app/stores/queueStore.ts +66 -1
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
|
@@ -3,19 +3,22 @@ import { create } from "zustand";
|
|
|
3
3
|
const API_BASE = `http://localhost:${(window as any).PORT}`;
|
|
4
4
|
|
|
5
5
|
/** Models that have a dedicated download endpoint in the backend. */
|
|
6
|
-
export type AiModelId = "z-image" | "flux" | "
|
|
6
|
+
export type AiModelId = "z-image" | "flux" | "ltx" | "tts" | "gemma";
|
|
7
7
|
|
|
8
8
|
/** Tools whose "install" step must run before their models can be downloaded. */
|
|
9
|
-
export type AiToolId = "mlxgen" | "mlx-vlm";
|
|
9
|
+
export type AiToolId = "mlxgen" | "mlx-vlm" | "hf-cli";
|
|
10
10
|
|
|
11
11
|
interface AiModelStore {
|
|
12
12
|
// Tool installation status
|
|
13
13
|
mlxgenInstalled: boolean | null;
|
|
14
14
|
mlxVlmInstalled: boolean | null;
|
|
15
|
+
hfInstalled: boolean | null;
|
|
15
16
|
// Model download status
|
|
16
17
|
zImageDownloaded: boolean | null;
|
|
17
18
|
fluxDownloaded: boolean | null;
|
|
18
|
-
|
|
19
|
+
ltxDownloaded: boolean | null;
|
|
20
|
+
ttsDownloaded: boolean | null;
|
|
21
|
+
gemmaDownloaded: boolean | null;
|
|
19
22
|
// In-flight install/download (a model id or tool id)
|
|
20
23
|
downloading: string | null;
|
|
21
24
|
logs: string[];
|
|
@@ -65,40 +68,50 @@ async function readSSE(
|
|
|
65
68
|
const DOWNLOAD_ENDPOINTS: Record<AiModelId, string> = {
|
|
66
69
|
"z-image": "/api/mlxgen/download-z-model",
|
|
67
70
|
flux: "/api/mlxgen/download-flux-model",
|
|
68
|
-
|
|
71
|
+
ltx: "/api/hf/download-ltx",
|
|
72
|
+
tts: "/api/hf/download-tts",
|
|
73
|
+
gemma: "/api/hf/download-mlx-vlm",
|
|
69
74
|
};
|
|
70
75
|
|
|
71
76
|
const TOOL_ENDPOINTS: Record<AiToolId, string> = {
|
|
72
77
|
mlxgen: "/api/mlxgen/install",
|
|
73
78
|
"mlx-vlm": "/api/agent/install",
|
|
79
|
+
"hf-cli": "/api/hf/install",
|
|
74
80
|
};
|
|
75
81
|
|
|
76
82
|
export const useAiModelStore = create<AiModelStore>((set, get) => ({
|
|
77
83
|
mlxgenInstalled: null,
|
|
78
84
|
mlxVlmInstalled: null,
|
|
85
|
+
hfInstalled: null,
|
|
79
86
|
zImageDownloaded: null,
|
|
80
87
|
fluxDownloaded: null,
|
|
81
|
-
|
|
88
|
+
ltxDownloaded: null,
|
|
89
|
+
ttsDownloaded: null,
|
|
90
|
+
gemmaDownloaded: null,
|
|
82
91
|
downloading: null,
|
|
83
92
|
logs: [],
|
|
84
93
|
error: null,
|
|
85
94
|
|
|
86
95
|
checkStatus: async () => {
|
|
87
96
|
try {
|
|
88
|
-
const [mlxgen, agent] = await Promise.all([
|
|
97
|
+
const [mlxgen, agent, hf] = await Promise.all([
|
|
89
98
|
fetch(`${API_BASE}/api/mlxgen/status`).then((r) =>
|
|
90
99
|
r.ok ? r.json() : null,
|
|
91
100
|
),
|
|
92
101
|
fetch(`${API_BASE}/api/agent/status`).then((r) =>
|
|
93
102
|
r.ok ? r.json() : null,
|
|
94
103
|
),
|
|
104
|
+
fetch(`${API_BASE}/api/hf/status`).then((r) => (r.ok ? r.json() : null)),
|
|
95
105
|
]);
|
|
96
106
|
set({
|
|
97
107
|
mlxgenInstalled: mlxgen ? Boolean(mlxgen.installed) : null,
|
|
98
|
-
qwenDownloaded: mlxgen ? Boolean(mlxgen.modelDownloaded) : null,
|
|
99
108
|
zImageDownloaded: mlxgen ? Boolean(mlxgen.zModelDownloaded) : null,
|
|
100
109
|
fluxDownloaded: mlxgen ? Boolean(mlxgen.fluxModelDownloaded) : null,
|
|
101
110
|
mlxVlmInstalled: agent ? Boolean(agent.installed) : null,
|
|
111
|
+
hfInstalled: hf ? Boolean(hf.installed) : null,
|
|
112
|
+
ltxDownloaded: hf ? Boolean(hf.ltxDownloaded) : null,
|
|
113
|
+
ttsDownloaded: hf ? Boolean(hf.ttsDownloaded) : null,
|
|
114
|
+
gemmaDownloaded: hf ? Boolean(hf.mlxVlmDownloaded) : null,
|
|
102
115
|
});
|
|
103
116
|
} catch {
|
|
104
117
|
// Leave status unknown (null) if the checks fail.
|
|
@@ -30,6 +30,98 @@ function playDing3x() {
|
|
|
30
30
|
/** Track task ids whose completed result has already been applied. */
|
|
31
31
|
const appliedCompleted = new Set<string>();
|
|
32
32
|
|
|
33
|
+
/** Track task ids already counted toward their batch's completion. */
|
|
34
|
+
const countedBatch = new Set<string>();
|
|
35
|
+
|
|
36
|
+
interface BatchTracker {
|
|
37
|
+
kind: "assets" | "sceneImages" | "videos" | "render";
|
|
38
|
+
total: number;
|
|
39
|
+
finished: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** In-flight bulk render batches, keyed by the batchId stamped on each task. */
|
|
43
|
+
const batches = new Map<string, BatchTracker>();
|
|
44
|
+
|
|
45
|
+
function makeBatchId(): string {
|
|
46
|
+
try {
|
|
47
|
+
return crypto.randomUUID();
|
|
48
|
+
} catch {
|
|
49
|
+
return `b-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Reset the aggregate spinner once a batch finishes and play a completion ding. */
|
|
54
|
+
function finalizeBatch(batchId: string, set: (patch: any) => void): void {
|
|
55
|
+
const b = batches.get(batchId);
|
|
56
|
+
if (!b) return;
|
|
57
|
+
batches.delete(batchId);
|
|
58
|
+
switch (b.kind) {
|
|
59
|
+
case "assets":
|
|
60
|
+
set({
|
|
61
|
+
assetsRendering: false,
|
|
62
|
+
assetStatus: b.finished > 0 ? "Assets rendered" : null,
|
|
63
|
+
});
|
|
64
|
+
break;
|
|
65
|
+
case "sceneImages":
|
|
66
|
+
set({
|
|
67
|
+
sceneImagesRendering: false,
|
|
68
|
+
sceneImageStatus: b.finished > 0 ? "Scene images rendered" : null,
|
|
69
|
+
sceneImageProgress: null,
|
|
70
|
+
});
|
|
71
|
+
break;
|
|
72
|
+
case "videos":
|
|
73
|
+
set({
|
|
74
|
+
videosRendering: false,
|
|
75
|
+
videoStatus: b.finished > 0 ? "Videos rendered" : null,
|
|
76
|
+
videoProgress: null,
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
case "render":
|
|
80
|
+
set({
|
|
81
|
+
rendering: false,
|
|
82
|
+
renderStatus: b.finished > 0 ? "Render complete" : null,
|
|
83
|
+
renderProgress: null,
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (b.finished > 0) playDing3x();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Count one finished batch task; update progress and finalize when complete. */
|
|
91
|
+
function finishBatch(batchId: string, set: (patch: any) => void): void {
|
|
92
|
+
const b = batches.get(batchId);
|
|
93
|
+
if (!b) return;
|
|
94
|
+
b.finished += 1;
|
|
95
|
+
if (b.finished < b.total) {
|
|
96
|
+
const status = `${b.finished}/${b.total} rendered`;
|
|
97
|
+
if (b.kind === "assets") set({ assetStatus: status });
|
|
98
|
+
else if (b.kind === "sceneImages") set({ sceneImageStatus: status });
|
|
99
|
+
else if (b.kind === "videos") set({ videoStatus: status });
|
|
100
|
+
else if (b.kind === "render") set({ renderStatus: status });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
finalizeBatch(batchId, set);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Surface a per-item failure and count it toward its batch on terminal status. */
|
|
107
|
+
function handleBatchTerminal(task: QueueTask, set: (patch: any) => void): void {
|
|
108
|
+
const batchId = task.payload?.batchId;
|
|
109
|
+
if (!batchId) return;
|
|
110
|
+
if (countedBatch.has(task.id)) return;
|
|
111
|
+
countedBatch.add(task.id);
|
|
112
|
+
|
|
113
|
+
if (task.status === "failed") {
|
|
114
|
+
const b = batches.get(batchId);
|
|
115
|
+
if (b) {
|
|
116
|
+
if (b.kind === "assets") set({ assetsError: task.error });
|
|
117
|
+
else if (b.kind === "sceneImages") set({ sceneImagesError: task.error });
|
|
118
|
+
else if (b.kind === "videos") set({ videosError: task.error });
|
|
119
|
+
else if (b.kind === "render") set({ renderError: task.error });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finishBatch(batchId, set);
|
|
123
|
+
}
|
|
124
|
+
|
|
33
125
|
export interface MovieCharacter {
|
|
34
126
|
slug: string;
|
|
35
127
|
name: string;
|
|
@@ -116,6 +208,18 @@ function upsertSceneImage(
|
|
|
116
208
|
return [...images, { slug, filename: "", url: "", updatedAt: 0, ...patch }];
|
|
117
209
|
}
|
|
118
210
|
|
|
211
|
+
function upsertRenderedScene(
|
|
212
|
+
scenes: RenderedScene[],
|
|
213
|
+
slug: string,
|
|
214
|
+
patch: Partial<RenderedScene>,
|
|
215
|
+
): RenderedScene[] {
|
|
216
|
+
const existing = scenes.find((s) => s.slug === slug);
|
|
217
|
+
if (existing) {
|
|
218
|
+
return scenes.map((s) => (s.slug === slug ? { ...s, ...patch } : s));
|
|
219
|
+
}
|
|
220
|
+
return [...scenes, { slug, imageUrl: null, videoUrl: null, ...patch }];
|
|
221
|
+
}
|
|
222
|
+
|
|
119
223
|
/** Enqueue a generation task in the backend worker. */
|
|
120
224
|
async function enqueueTask(
|
|
121
225
|
projectId: string,
|
|
@@ -143,6 +247,8 @@ interface MovieStudioStore {
|
|
|
143
247
|
projectId: string | null;
|
|
144
248
|
hydrated: boolean;
|
|
145
249
|
generating: boolean;
|
|
250
|
+
generateStatus: string | null;
|
|
251
|
+
generateProgress: { current: number; total: number } | null;
|
|
146
252
|
result: MovieStudioResult | null;
|
|
147
253
|
error: string | null;
|
|
148
254
|
rendering: boolean;
|
|
@@ -197,6 +303,8 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
197
303
|
projectId: null,
|
|
198
304
|
hydrated: false,
|
|
199
305
|
generating: false,
|
|
306
|
+
generateStatus: null,
|
|
307
|
+
generateProgress: null,
|
|
200
308
|
result: null,
|
|
201
309
|
error: null,
|
|
202
310
|
rendering: false,
|
|
@@ -250,6 +358,9 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
250
358
|
assets: Array.isArray(stored.assets) ? stored.assets : [],
|
|
251
359
|
videos: Array.isArray(stored.videos) ? stored.videos : [],
|
|
252
360
|
sceneImages: Array.isArray(stored.sceneImages) ? stored.sceneImages : [],
|
|
361
|
+
renderedScenes: Array.isArray(stored.renderedScenes)
|
|
362
|
+
? stored.renderedScenes
|
|
363
|
+
: [],
|
|
253
364
|
});
|
|
254
365
|
} catch {
|
|
255
366
|
// Ignore — keep in-memory defaults.
|
|
@@ -274,6 +385,30 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
274
385
|
const result = get().result;
|
|
275
386
|
if (!result || get().rendering) return;
|
|
276
387
|
|
|
388
|
+
const assets: { kind: "character" | "place"; slug: string; prompt: string }[] = [
|
|
389
|
+
...result.characters
|
|
390
|
+
.filter((c) => c.slug && String(c.imagePrompt || "").trim())
|
|
391
|
+
.map((c) => ({
|
|
392
|
+
kind: "character" as const,
|
|
393
|
+
slug: c.slug,
|
|
394
|
+
prompt: c.imagePrompt,
|
|
395
|
+
})),
|
|
396
|
+
...result.places
|
|
397
|
+
.filter((p) => p.slug && String(p.imagePrompt || "").trim())
|
|
398
|
+
.map((p) => ({
|
|
399
|
+
kind: "place" as const,
|
|
400
|
+
slug: p.slug,
|
|
401
|
+
prompt: p.imagePrompt,
|
|
402
|
+
})),
|
|
403
|
+
];
|
|
404
|
+
const scenes = result.scenes.filter((s) => s.slug);
|
|
405
|
+
// One queue task per output: each asset image, scene image, and scene video.
|
|
406
|
+
const total = assets.length + scenes.length * 2;
|
|
407
|
+
if (total === 0) return;
|
|
408
|
+
|
|
409
|
+
const batchId = makeBatchId();
|
|
410
|
+
batches.set(batchId, { kind: "render", total, finished: 0 });
|
|
411
|
+
|
|
277
412
|
set({
|
|
278
413
|
rendering: true,
|
|
279
414
|
renderStatus: "Queued…",
|
|
@@ -281,26 +416,93 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
281
416
|
renderError: null,
|
|
282
417
|
renderProgress: null,
|
|
283
418
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
419
|
+
|
|
420
|
+
// FIFO order matters: assets → scene images → scene videos.
|
|
421
|
+
for (const item of assets) {
|
|
422
|
+
const r = await enqueueTask(
|
|
423
|
+
projectId,
|
|
424
|
+
"render-asset",
|
|
425
|
+
`Render ${item.kind}: ${item.slug}`,
|
|
426
|
+
{ ...item, batchId },
|
|
427
|
+
);
|
|
428
|
+
if (!r.ok) {
|
|
429
|
+
const b = batches.get(batchId);
|
|
430
|
+
if (b) b.total -= 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
for (const scene of scenes) {
|
|
434
|
+
const r = await enqueueTask(
|
|
435
|
+
projectId,
|
|
436
|
+
"render-scene-image",
|
|
437
|
+
`Render scene image: ${scene.slug}`,
|
|
438
|
+
{ scene, batchId },
|
|
439
|
+
);
|
|
440
|
+
if (!r.ok) {
|
|
441
|
+
const b = batches.get(batchId);
|
|
442
|
+
if (b) b.total -= 1;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const scene of scenes) {
|
|
446
|
+
const r = await enqueueTask(
|
|
447
|
+
projectId,
|
|
448
|
+
"render-video",
|
|
449
|
+
`Render video: ${scene.slug}`,
|
|
450
|
+
{ scene, characters: result.characters, batchId },
|
|
451
|
+
);
|
|
452
|
+
if (!r.ok) {
|
|
453
|
+
const b = batches.get(batchId);
|
|
454
|
+
if (b) b.total -= 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// If nothing could be enqueued, reset the spinner immediately.
|
|
459
|
+
const b = batches.get(batchId);
|
|
460
|
+
if (b && b.total <= b.finished) finalizeBatch(batchId, set);
|
|
290
461
|
},
|
|
291
462
|
|
|
292
463
|
renderAssets: async (projectId) => {
|
|
293
464
|
const result = get().result;
|
|
294
465
|
if (!result || get().assetsRendering) return;
|
|
295
466
|
|
|
467
|
+
const items: { kind: "character" | "place"; slug: string; prompt: string }[] = [
|
|
468
|
+
...result.characters
|
|
469
|
+
.filter((c) => c.slug && String(c.imagePrompt || "").trim())
|
|
470
|
+
.map((c) => ({
|
|
471
|
+
kind: "character" as const,
|
|
472
|
+
slug: c.slug,
|
|
473
|
+
prompt: c.imagePrompt,
|
|
474
|
+
})),
|
|
475
|
+
...result.places
|
|
476
|
+
.filter((p) => p.slug && String(p.imagePrompt || "").trim())
|
|
477
|
+
.map((p) => ({
|
|
478
|
+
kind: "place" as const,
|
|
479
|
+
slug: p.slug,
|
|
480
|
+
prompt: p.imagePrompt,
|
|
481
|
+
})),
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
if (items.length === 0) return;
|
|
485
|
+
|
|
486
|
+
const batchId = makeBatchId();
|
|
487
|
+
batches.set(batchId, { kind: "assets", total: items.length, finished: 0 });
|
|
488
|
+
|
|
296
489
|
set({ assetsRendering: true, assetStatus: "Queued…", assetsError: null });
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
490
|
+
|
|
491
|
+
for (const item of items) {
|
|
492
|
+
const r = await enqueueTask(
|
|
493
|
+
projectId,
|
|
494
|
+
"render-asset",
|
|
495
|
+
`Render ${item.kind}: ${item.slug}`,
|
|
496
|
+
{ ...item, batchId },
|
|
497
|
+
);
|
|
498
|
+
if (!r.ok) {
|
|
499
|
+
const b = batches.get(batchId);
|
|
500
|
+
if (b) b.total -= 1;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const b = batches.get(batchId);
|
|
505
|
+
if (b && b.total <= b.finished) finalizeBatch(batchId, set);
|
|
304
506
|
},
|
|
305
507
|
|
|
306
508
|
regenerateAsset: async (projectId, kind, slug, prompt) => {
|
|
@@ -329,19 +531,34 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
329
531
|
const result = get().result;
|
|
330
532
|
if (!result || get().videosRendering) return;
|
|
331
533
|
|
|
534
|
+
const scenes = result.scenes.filter((s) => s.slug);
|
|
535
|
+
if (scenes.length === 0) return;
|
|
536
|
+
|
|
537
|
+
const batchId = makeBatchId();
|
|
538
|
+
batches.set(batchId, { kind: "videos", total: scenes.length, finished: 0 });
|
|
539
|
+
|
|
332
540
|
set({
|
|
333
541
|
videosRendering: true,
|
|
334
542
|
videoStatus: "Queued…",
|
|
335
543
|
videosError: null,
|
|
336
544
|
videoProgress: null,
|
|
337
545
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
546
|
+
|
|
547
|
+
for (const scene of scenes) {
|
|
548
|
+
const r = await enqueueTask(
|
|
549
|
+
projectId,
|
|
550
|
+
"render-video",
|
|
551
|
+
`Render video: ${scene.slug}`,
|
|
552
|
+
{ scene, characters: result.characters, batchId },
|
|
553
|
+
);
|
|
554
|
+
if (!r.ok) {
|
|
555
|
+
const b = batches.get(batchId);
|
|
556
|
+
if (b) b.total -= 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const b = batches.get(batchId);
|
|
561
|
+
if (b && b.total <= b.finished) finalizeBatch(batchId, set);
|
|
345
562
|
},
|
|
346
563
|
|
|
347
564
|
regenerateVideo: async (projectId, slug) => {
|
|
@@ -372,19 +589,38 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
372
589
|
const result = get().result;
|
|
373
590
|
if (!result || get().sceneImagesRendering) return;
|
|
374
591
|
|
|
592
|
+
const scenes = result.scenes.filter((s) => s.slug);
|
|
593
|
+
if (scenes.length === 0) return;
|
|
594
|
+
|
|
595
|
+
const batchId = makeBatchId();
|
|
596
|
+
batches.set(batchId, {
|
|
597
|
+
kind: "sceneImages",
|
|
598
|
+
total: scenes.length,
|
|
599
|
+
finished: 0,
|
|
600
|
+
});
|
|
601
|
+
|
|
375
602
|
set({
|
|
376
603
|
sceneImagesRendering: true,
|
|
377
604
|
sceneImageStatus: "Queued…",
|
|
378
605
|
sceneImagesError: null,
|
|
379
606
|
sceneImageProgress: null,
|
|
380
607
|
});
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
608
|
+
|
|
609
|
+
for (const scene of scenes) {
|
|
610
|
+
const r = await enqueueTask(
|
|
611
|
+
projectId,
|
|
612
|
+
"render-scene-image",
|
|
613
|
+
`Render scene image: ${scene.slug}`,
|
|
614
|
+
{ scene, batchId },
|
|
615
|
+
);
|
|
616
|
+
if (!r.ok) {
|
|
617
|
+
const b = batches.get(batchId);
|
|
618
|
+
if (b) b.total -= 1;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const b = batches.get(batchId);
|
|
623
|
+
if (b && b.total <= b.finished) finalizeBatch(batchId, set);
|
|
388
624
|
},
|
|
389
625
|
|
|
390
626
|
regenerateSceneImage: async (projectId, slug) => {
|
|
@@ -459,20 +695,40 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
459
695
|
applyQueueTask: (task) => {
|
|
460
696
|
const isActive = task.status === "pending" || task.status === "running";
|
|
461
697
|
const err = task.status === "failed" ? task.error : null;
|
|
698
|
+
const terminal =
|
|
699
|
+
task.status === "completed" ||
|
|
700
|
+
task.status === "failed" ||
|
|
701
|
+
task.status === "cancelled" ||
|
|
702
|
+
task.status === "paused";
|
|
703
|
+
const batchKind = batches.get(task.payload?.batchId)?.kind;
|
|
462
704
|
|
|
463
705
|
switch (task.type) {
|
|
464
706
|
case "generate": {
|
|
465
707
|
if (task.status === "completed" && task.result) {
|
|
466
708
|
if (!appliedCompleted.has(task.id)) {
|
|
467
709
|
appliedCompleted.add(task.id);
|
|
468
|
-
set({
|
|
710
|
+
set({
|
|
711
|
+
result: task.result,
|
|
712
|
+
generating: false,
|
|
713
|
+
generateStatus: null,
|
|
714
|
+
generateProgress: null,
|
|
715
|
+
});
|
|
469
716
|
persistMovieStudioState();
|
|
470
717
|
playDing3x();
|
|
471
718
|
}
|
|
472
719
|
} else if (err) {
|
|
473
|
-
set({
|
|
720
|
+
set({
|
|
721
|
+
generating: false,
|
|
722
|
+
error: err,
|
|
723
|
+
generateStatus: null,
|
|
724
|
+
generateProgress: null,
|
|
725
|
+
});
|
|
474
726
|
} else {
|
|
475
|
-
set({
|
|
727
|
+
set({
|
|
728
|
+
generating: isActive,
|
|
729
|
+
generateStatus: task.status === "running" ? task.statusText : null,
|
|
730
|
+
generateProgress: task.progress,
|
|
731
|
+
});
|
|
476
732
|
}
|
|
477
733
|
break;
|
|
478
734
|
}
|
|
@@ -588,6 +844,69 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
588
844
|
break;
|
|
589
845
|
}
|
|
590
846
|
|
|
847
|
+
case "render-asset": {
|
|
848
|
+
const key = `${task.payload?.kind}:${task.payload?.slug}`;
|
|
849
|
+
if (task.status === "completed" && task.result) {
|
|
850
|
+
if (!appliedCompleted.has(task.id)) {
|
|
851
|
+
appliedCompleted.add(task.id);
|
|
852
|
+
const r = task.result;
|
|
853
|
+
set((s) => ({
|
|
854
|
+
assets: [
|
|
855
|
+
...s.assets.filter((a) => `${a.kind}:${a.slug}` !== key),
|
|
856
|
+
r,
|
|
857
|
+
],
|
|
858
|
+
}));
|
|
859
|
+
persistMovieStudioState();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (terminal) handleBatchTerminal(task, set);
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
case "render-scene-image": {
|
|
867
|
+
const slug = task.payload?.slug;
|
|
868
|
+
if (task.status === "completed" && task.result) {
|
|
869
|
+
if (!appliedCompleted.has(task.id)) {
|
|
870
|
+
appliedCompleted.add(task.id);
|
|
871
|
+
const r = task.result;
|
|
872
|
+
set((s) => ({
|
|
873
|
+
sceneImages: upsertSceneImage(s.sceneImages, slug, r),
|
|
874
|
+
renderedScenes:
|
|
875
|
+
batchKind === "render"
|
|
876
|
+
? upsertRenderedScene(s.renderedScenes, slug, {
|
|
877
|
+
imageUrl: r.url,
|
|
878
|
+
})
|
|
879
|
+
: s.renderedScenes,
|
|
880
|
+
}));
|
|
881
|
+
persistMovieStudioState();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (terminal) handleBatchTerminal(task, set);
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
case "render-video": {
|
|
889
|
+
const slug = task.payload?.slug;
|
|
890
|
+
if (task.status === "completed" && task.result) {
|
|
891
|
+
if (!appliedCompleted.has(task.id)) {
|
|
892
|
+
appliedCompleted.add(task.id);
|
|
893
|
+
const r = task.result;
|
|
894
|
+
set((s) => ({
|
|
895
|
+
videos: upsertVideo(s.videos, slug, r),
|
|
896
|
+
renderedScenes:
|
|
897
|
+
batchKind === "render"
|
|
898
|
+
? upsertRenderedScene(s.renderedScenes, slug, {
|
|
899
|
+
videoUrl: r.url,
|
|
900
|
+
})
|
|
901
|
+
: s.renderedScenes,
|
|
902
|
+
}));
|
|
903
|
+
persistMovieStudioState();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (terminal) handleBatchTerminal(task, set);
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
|
|
591
910
|
case "regenerate-asset": {
|
|
592
911
|
const key = `${task.payload?.kind}:${task.payload?.slug}`;
|
|
593
912
|
if (task.status === "completed" && task.result) {
|
|
@@ -683,6 +1002,8 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
683
1002
|
},
|
|
684
1003
|
|
|
685
1004
|
stop: () => {
|
|
1005
|
+
batches.clear();
|
|
1006
|
+
countedBatch.clear();
|
|
686
1007
|
fetch(`${API_BASE}/api/render/cancel`, { method: "POST" }).catch(() => {});
|
|
687
1008
|
const projectId = get().projectId;
|
|
688
1009
|
if (projectId) {
|
|
@@ -704,10 +1025,14 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
704
1025
|
});
|
|
705
1026
|
},
|
|
706
1027
|
|
|
707
|
-
reset: () =>
|
|
1028
|
+
reset: () => {
|
|
1029
|
+
batches.clear();
|
|
1030
|
+
countedBatch.clear();
|
|
708
1031
|
set({
|
|
709
1032
|
idea: "",
|
|
710
1033
|
generating: false,
|
|
1034
|
+
generateStatus: null,
|
|
1035
|
+
generateProgress: null,
|
|
711
1036
|
result: null,
|
|
712
1037
|
error: null,
|
|
713
1038
|
rendering: false,
|
|
@@ -733,7 +1058,8 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
|
|
|
733
1058
|
sceneImagesError: null,
|
|
734
1059
|
sceneImageProgress: null,
|
|
735
1060
|
regeneratingSceneImages: [],
|
|
736
|
-
})
|
|
1061
|
+
});
|
|
1062
|
+
},
|
|
737
1063
|
}));
|
|
738
1064
|
|
|
739
1065
|
function persistMovieStudioState() {
|
|
@@ -749,6 +1075,7 @@ function persistMovieStudioState() {
|
|
|
749
1075
|
assets: s.assets,
|
|
750
1076
|
videos: s.videos,
|
|
751
1077
|
sceneImages: s.sceneImages,
|
|
1078
|
+
renderedScenes: s.renderedScenes,
|
|
752
1079
|
}),
|
|
753
1080
|
}).catch(() => {});
|
|
754
1081
|
}
|