@effectnode/media 0.1.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 (133) hide show
  1. package/bin/effectnode-media +2 -0
  2. package/dist/backend/index.d.ts +8 -0
  3. package/dist/backend/index.js +13 -0
  4. package/dist/backend/movie-backend/agent/agent-backend.d.ts +16 -0
  5. package/dist/backend/movie-backend/agent/agent-backend.js +703 -0
  6. package/dist/backend/movie-backend/agent/storyprompt.d.ts +1 -0
  7. package/dist/backend/movie-backend/agent/storyprompt.js +90 -0
  8. package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +3 -0
  9. package/dist/backend/movie-backend/agent/tools/edit-image.js +192 -0
  10. package/dist/backend/movie-backend/agent/tools/get-time.d.ts +3 -0
  11. package/dist/backend/movie-backend/agent/tools/get-time.js +7 -0
  12. package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +3 -0
  13. package/dist/backend/movie-backend/agent/tools/grep-files.js +59 -0
  14. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +3 -0
  15. package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +160 -0
  16. package/dist/backend/movie-backend/agent/tools/index.d.ts +7 -0
  17. package/dist/backend/movie-backend/agent/tools/index.js +59 -0
  18. package/dist/backend/movie-backend/agent/tools/list-files.d.ts +3 -0
  19. package/dist/backend/movie-backend/agent/tools/list-files.js +17 -0
  20. package/dist/backend/movie-backend/agent/tools/read-file.d.ts +3 -0
  21. package/dist/backend/movie-backend/agent/tools/read-file.js +28 -0
  22. package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +3 -0
  23. package/dist/backend/movie-backend/agent/tools/remove-file.js +22 -0
  24. package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +3 -0
  25. package/dist/backend/movie-backend/agent/tools/rename-file.js +37 -0
  26. package/dist/backend/movie-backend/agent/tools/show-image.d.ts +3 -0
  27. package/dist/backend/movie-backend/agent/tools/show-image.js +28 -0
  28. package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +3 -0
  29. package/dist/backend/movie-backend/agent/tools/stitch-videos.js +96 -0
  30. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +3 -0
  31. package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +125 -0
  32. package/dist/backend/movie-backend/agent/tools/types.d.ts +15 -0
  33. package/dist/backend/movie-backend/agent/tools/types.js +1 -0
  34. package/dist/backend/movie-backend/agent/tools/update-file.d.ts +3 -0
  35. package/dist/backend/movie-backend/agent/tools/update-file.js +27 -0
  36. package/dist/backend/movie-backend/agent/tools/write-file.d.ts +3 -0
  37. package/dist/backend/movie-backend/agent/tools/write-file.js +26 -0
  38. package/dist/backend/movie-backend/agent/workspace.d.ts +25 -0
  39. package/dist/backend/movie-backend/agent/workspace.js +124 -0
  40. package/dist/backend/movie-backend/core.d.ts +4 -0
  41. package/dist/backend/movie-backend/core.js +1167 -0
  42. package/dist/backend/movie-backend/generation-queue.d.ts +24 -0
  43. package/dist/backend/movie-backend/generation-queue.js +766 -0
  44. package/dist/backend/movie-backend/process.d.ts +21 -0
  45. package/dist/backend/movie-backend/process.js +98 -0
  46. package/dist/backend/movie-backend/render-media.d.ts +48 -0
  47. package/dist/backend/movie-backend/render-media.js +3311 -0
  48. package/dist/backend/routes/health.d.ts +1 -0
  49. package/dist/backend/routes/health.js +9 -0
  50. package/dist/backend/routes/index.d.ts +1 -0
  51. package/dist/backend/routes/index.js +11 -0
  52. package/dist/backend/routes/media.d.ts +1 -0
  53. package/dist/backend/routes/media.js +13 -0
  54. package/dist/backend/routes/projects.d.ts +8 -0
  55. package/dist/backend/routes/projects.js +68 -0
  56. package/dist/backend/routes/voice.d.ts +1 -0
  57. package/dist/backend/routes/voice.js +39 -0
  58. package/dist/backend/routes.d.ts +1 -0
  59. package/dist/backend/routes.js +20 -0
  60. package/dist/backend/server.d.ts +1 -0
  61. package/dist/backend/server.js +23 -0
  62. package/dist/backend/workspace.d.ts +5 -0
  63. package/dist/backend/workspace.js +27 -0
  64. package/dist/backend/ws/index.d.ts +3 -0
  65. package/dist/backend/ws/index.js +27 -0
  66. package/dist/backend/ws/messages.d.ts +8 -0
  67. package/dist/backend/ws/messages.js +21 -0
  68. package/dist/backend/ws.d.ts +3 -0
  69. package/dist/backend/ws.js +29 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +80 -0
  72. package/frontend/index.html +22 -0
  73. package/frontend/public/ffmpeg/ffmpeg-core.js +16 -0
  74. package/frontend/public/ffmpeg/ffmpeg-core.wasm +0 -0
  75. package/frontend/src/index.css +46 -0
  76. package/frontend/src/main.tsx +13 -0
  77. package/frontend/src/movie-app/AppRouter.tsx +14 -0
  78. package/frontend/src/movie-app/MediaStudio.tsx +9 -0
  79. package/frontend/src/movie-app/SetupPage.tsx +293 -0
  80. package/frontend/src/movie-app/components/AgentWorkspace.tsx +574 -0
  81. package/frontend/src/movie-app/components/ChatUI/ChatUI.tsx +655 -0
  82. package/frontend/src/movie-app/components/ChatUI/MarkdownMessage.tsx +267 -0
  83. package/frontend/src/movie-app/components/EditorTabs/AgentTab.tsx +61 -0
  84. package/frontend/src/movie-app/components/EditorTabs/BatchImageToVideoTab.tsx +927 -0
  85. package/frontend/src/movie-app/components/EditorTabs/BatchVideoTab.tsx +777 -0
  86. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +913 -0
  87. package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +234 -0
  88. package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +664 -0
  89. package/frontend/src/movie-app/components/EditorTabs/CropTool.tsx +251 -0
  90. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +305 -0
  91. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +249 -0
  92. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +555 -0
  93. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +722 -0
  94. package/frontend/src/movie-app/components/EditorTabs/LlmServerTab.tsx +40 -0
  95. package/frontend/src/movie-app/components/EditorTabs/MlxVlmServerPanel.tsx +490 -0
  96. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +938 -0
  97. package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +881 -0
  98. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +267 -0
  99. package/frontend/src/movie-app/components/EditorTabs/StoryWriterTab.tsx +51 -0
  100. package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +246 -0
  101. package/frontend/src/movie-app/components/EditorTabs/TerminalLogPanel.tsx +49 -0
  102. package/frontend/src/movie-app/components/EditorTabs/TextToImageTab.tsx +574 -0
  103. package/frontend/src/movie-app/components/EditorTabs/VoiceChatPanel.tsx +320 -0
  104. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +636 -0
  105. package/frontend/src/movie-app/components/ProjectManager.tsx +381 -0
  106. package/frontend/src/movie-app/index.css +46 -0
  107. package/frontend/src/movie-app/index.html +16 -0
  108. package/frontend/src/movie-app/lambobo.png +0 -0
  109. package/frontend/src/movie-app/lib/batchImageToVideoStorage.ts +69 -0
  110. package/frontend/src/movie-app/lib/batchVideoStorage.ts +68 -0
  111. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +75 -0
  112. package/frontend/src/movie-app/lib/ffmpeg.ts +42 -0
  113. package/frontend/src/movie-app/lib/referencesToVideoStorage.ts +64 -0
  114. package/frontend/src/movie-app/lib/textToImageStorage.ts +61 -0
  115. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.js +16 -0
  116. package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.wasm +0 -0
  117. package/frontend/src/movie-app/stores/batchImageToVideoStore.ts +736 -0
  118. package/frontend/src/movie-app/stores/batchVideoStore.ts +670 -0
  119. package/frontend/src/movie-app/stores/batchVoiceStore.ts +990 -0
  120. package/frontend/src/movie-app/stores/characterStore.ts +109 -0
  121. package/frontend/src/movie-app/stores/chatStore.ts +459 -0
  122. package/frontend/src/movie-app/stores/generationStore.ts +2140 -0
  123. package/frontend/src/movie-app/stores/logStore.ts +25 -0
  124. package/frontend/src/movie-app/stores/movieStudioStore.ts +709 -0
  125. package/frontend/src/movie-app/stores/projectStore.ts +124 -0
  126. package/frontend/src/movie-app/stores/queueStore.ts +214 -0
  127. package/frontend/src/movie-app/stores/referencesToVideoStore.ts +467 -0
  128. package/frontend/src/movie-app/stores/sceneVisualStore.ts +251 -0
  129. package/frontend/src/movie-app/stores/voiceChatStore.ts +170 -0
  130. package/frontend/src/movie-app/stores/workspaceStore.ts +158 -0
  131. package/frontend/src/vite-env.d.ts +1 -0
  132. package/frontend/tsconfig.json +19 -0
  133. package/package.json +65 -0
@@ -0,0 +1,709 @@
1
+ import { create } from "zustand";
2
+ import type { QueueTask } from "./queueStore";
3
+
4
+ const API_BASE = `http://localhost:${(window as any).PORT}`;
5
+
6
+ /** Play three short "ding" sounds to signal a finished generation task. */
7
+ function playDing3x() {
8
+ try {
9
+ const Ctx = window.AudioContext || (window as any).webkitAudioContext;
10
+ const ctx = new Ctx();
11
+ for (let i = 0; i < 3; i++) {
12
+ const t = ctx.currentTime + i * 0.35;
13
+ const osc = ctx.createOscillator();
14
+ const gain = ctx.createGain();
15
+ osc.type = "sine";
16
+ osc.frequency.value = 880;
17
+ gain.gain.setValueAtTime(0.0001, t);
18
+ gain.gain.exponentialRampToValueAtTime(0.25, t + 0.02);
19
+ gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.3);
20
+ osc.connect(gain);
21
+ gain.connect(ctx.destination);
22
+ osc.start(t);
23
+ osc.stop(t + 0.32);
24
+ }
25
+ } catch {
26
+ // silently ignore if audio is unavailable
27
+ }
28
+ }
29
+
30
+ /** Track task ids whose completed result has already been applied. */
31
+ const appliedCompleted = new Set<string>();
32
+
33
+ export interface MovieCharacter {
34
+ slug: string;
35
+ name: string;
36
+ imagePrompt: string;
37
+ }
38
+
39
+ export interface MoviePlace {
40
+ slug: string;
41
+ name: string;
42
+ imagePrompt: string;
43
+ }
44
+
45
+ export interface MovieScriptLine {
46
+ characterSlug: string;
47
+ line: string;
48
+ }
49
+
50
+ export interface MovieScene {
51
+ slug: string;
52
+ duration: number;
53
+ description: string;
54
+ characterSlugs: string[];
55
+ placeSlug: string;
56
+ scriptLines: MovieScriptLine[];
57
+ voiceOver: string;
58
+ imagePrompt: string;
59
+ }
60
+
61
+ export interface MovieStudioResult {
62
+ characters: MovieCharacter[];
63
+ places: MoviePlace[];
64
+ scenes: MovieScene[];
65
+ }
66
+
67
+ export interface AssetImage {
68
+ kind: "character" | "place";
69
+ slug: string;
70
+ filename: string;
71
+ url: string;
72
+ updatedAt: number;
73
+ }
74
+
75
+ export interface RenderedScene {
76
+ slug: string;
77
+ imageUrl: string | null;
78
+ videoUrl: string | null;
79
+ }
80
+
81
+ export interface SceneVideo {
82
+ slug: string;
83
+ filename: string;
84
+ url: string;
85
+ updatedAt: number;
86
+ }
87
+
88
+ function upsertVideo(
89
+ videos: SceneVideo[],
90
+ slug: string,
91
+ patch: Partial<SceneVideo>,
92
+ ): SceneVideo[] {
93
+ const existing = videos.find((v) => v.slug === slug);
94
+ if (existing) {
95
+ return videos.map((v) => (v.slug === slug ? { ...v, ...patch } : v));
96
+ }
97
+ return [...videos, { slug, filename: "", url: "", updatedAt: 0, ...patch }];
98
+ }
99
+
100
+ export interface SceneImage {
101
+ slug: string;
102
+ filename: string;
103
+ url: string;
104
+ updatedAt: number;
105
+ }
106
+
107
+ function upsertSceneImage(
108
+ images: SceneImage[],
109
+ slug: string,
110
+ patch: Partial<SceneImage>,
111
+ ): SceneImage[] {
112
+ const existing = images.find((v) => v.slug === slug);
113
+ if (existing) {
114
+ return images.map((v) => (v.slug === slug ? { ...v, ...patch } : v));
115
+ }
116
+ return [...images, { slug, filename: "", url: "", updatedAt: 0, ...patch }];
117
+ }
118
+
119
+ /** Enqueue a generation task in the backend worker. */
120
+ async function enqueueTask(
121
+ projectId: string,
122
+ type: string,
123
+ label: string,
124
+ payload: any,
125
+ ): Promise<{ ok: boolean; error?: string }> {
126
+ try {
127
+ const res = await fetch(`${API_BASE}/api/queue/enqueue`, {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ projectId, type, label, payload }),
131
+ });
132
+ if (!res.ok) {
133
+ return { ok: false, error: await res.text() };
134
+ }
135
+ return { ok: true };
136
+ } catch (e) {
137
+ return { ok: false, error: String(e) };
138
+ }
139
+ }
140
+
141
+ interface MovieStudioStore {
142
+ idea: string;
143
+ projectId: string | null;
144
+ hydrated: boolean;
145
+ generating: boolean;
146
+ result: MovieStudioResult | null;
147
+ error: string | null;
148
+ rendering: boolean;
149
+ renderStatus: string | null;
150
+ renderLogs: string[];
151
+ renderError: string | null;
152
+ renderProgress: { current: number; total: number } | null;
153
+ assets: AssetImage[];
154
+ assetsRendering: boolean;
155
+ assetStatus: string | null;
156
+ assetsError: string | null;
157
+ regenerating: string[];
158
+ renderedScenes: RenderedScene[];
159
+ videos: SceneVideo[];
160
+ videosRendering: boolean;
161
+ videoStatus: string | null;
162
+ videosError: string | null;
163
+ videoProgress: { current: number; total: number } | null;
164
+ regeneratingVideos: string[];
165
+ sceneImages: SceneImage[];
166
+ sceneImagesRendering: boolean;
167
+ sceneImageStatus: string | null;
168
+ sceneImagesError: string | null;
169
+ sceneImageProgress: { current: number; total: number } | null;
170
+ regeneratingSceneImages: string[];
171
+ setIdea: (v: string) => void;
172
+ hydrate: (projectId: string) => Promise<void>;
173
+ generate: (projectId: string, model: string) => Promise<void>;
174
+ render: (projectId: string) => Promise<void>;
175
+ renderAssets: (projectId: string) => Promise<void>;
176
+ regenerateAsset: (
177
+ projectId: string,
178
+ kind: "character" | "place",
179
+ slug: string,
180
+ prompt: string,
181
+ ) => Promise<void>;
182
+ renderVideos: (projectId: string) => Promise<void>;
183
+ regenerateVideo: (projectId: string, slug: string) => Promise<void>;
184
+ renderSceneImages: (projectId: string) => Promise<void>;
185
+ regenerateSceneImage: (projectId: string, slug: string) => Promise<void>;
186
+ applyQueueTask: (task: QueueTask) => void;
187
+ primeAppliedQueue: (tasks: QueueTask[]) => void;
188
+ stop: () => void;
189
+ reset: () => void;
190
+ }
191
+
192
+ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
193
+ idea: "",
194
+ projectId: null,
195
+ hydrated: false,
196
+ generating: false,
197
+ result: null,
198
+ error: null,
199
+ rendering: false,
200
+ renderStatus: null,
201
+ renderLogs: [],
202
+ renderError: null,
203
+ renderProgress: null,
204
+ assets: [],
205
+ assetsRendering: false,
206
+ assetStatus: null,
207
+ assetsError: null,
208
+ regenerating: [],
209
+ renderedScenes: [],
210
+ videos: [],
211
+ videosRendering: false,
212
+ videoStatus: null,
213
+ videosError: null,
214
+ videoProgress: null,
215
+ regeneratingVideos: [],
216
+ sceneImages: [],
217
+ sceneImagesRendering: false,
218
+ sceneImageStatus: null,
219
+ sceneImagesError: null,
220
+ sceneImageProgress: null,
221
+ regeneratingSceneImages: [],
222
+
223
+ setIdea: (idea) => {
224
+ set({ idea, error: null });
225
+ persistMovieStudioState();
226
+ },
227
+
228
+ hydrate: async (projectId) => {
229
+ // Switching projects: reset to defaults so the previous project's idea
230
+ // doesn't leak through, then load the stored state (if any) below.
231
+ const previous = get().projectId;
232
+ if (previous !== null && previous !== projectId) {
233
+ get().reset();
234
+ }
235
+ set({ hydrated: true, projectId });
236
+
237
+ try {
238
+ const res = await fetch(
239
+ `${API_BASE}/api/movie-studio/state?projectId=${encodeURIComponent(projectId)}`,
240
+ );
241
+ if (!res.ok) return;
242
+ const stored = await res.json();
243
+ if (!stored) return;
244
+ set({
245
+ idea: stored.idea ?? "",
246
+ result: stored.result ?? null,
247
+ assets: Array.isArray(stored.assets) ? stored.assets : [],
248
+ videos: Array.isArray(stored.videos) ? stored.videos : [],
249
+ sceneImages: Array.isArray(stored.sceneImages) ? stored.sceneImages : [],
250
+ });
251
+ } catch {
252
+ // Ignore — keep in-memory defaults.
253
+ }
254
+ },
255
+
256
+ generate: async (projectId, model) => {
257
+ const idea = get().idea.trim();
258
+ if (!idea || get().generating) return;
259
+
260
+ set({ generating: true, error: null });
261
+ const r = await enqueueTask(
262
+ projectId,
263
+ "generate",
264
+ "Generate production bible",
265
+ { idea, model },
266
+ );
267
+ if (!r.ok) set({ generating: false, error: r.error });
268
+ },
269
+
270
+ render: async (projectId) => {
271
+ const result = get().result;
272
+ if (!result || get().rendering) return;
273
+
274
+ set({
275
+ rendering: true,
276
+ renderStatus: "Queued…",
277
+ renderLogs: [],
278
+ renderError: null,
279
+ renderProgress: null,
280
+ });
281
+ const r = await enqueueTask(projectId, "render", "Render full movie", {
282
+ characters: result.characters,
283
+ places: result.places,
284
+ scenes: result.scenes,
285
+ });
286
+ if (!r.ok) set({ rendering: false, renderError: r.error });
287
+ },
288
+
289
+ renderAssets: async (projectId) => {
290
+ const result = get().result;
291
+ if (!result || get().assetsRendering) return;
292
+
293
+ set({ assetsRendering: true, assetStatus: "Queued…", assetsError: null });
294
+ const r = await enqueueTask(
295
+ projectId,
296
+ "render-assets",
297
+ "Render character & place images",
298
+ { characters: result.characters, places: result.places },
299
+ );
300
+ if (!r.ok) set({ assetsRendering: false, assetsError: r.error });
301
+ },
302
+
303
+ regenerateAsset: async (projectId, kind, slug, prompt) => {
304
+ const key = `${kind}:${slug}`;
305
+ if (get().regenerating.includes(key)) return;
306
+
307
+ set((s) => ({
308
+ regenerating: [...s.regenerating, key],
309
+ assetsError: null,
310
+ }));
311
+ const r = await enqueueTask(
312
+ projectId,
313
+ "regenerate-asset",
314
+ `Regenerate ${kind}: ${slug}`,
315
+ { kind, slug, prompt },
316
+ );
317
+ if (!r.ok) {
318
+ set((s) => ({
319
+ regenerating: s.regenerating.filter((k) => k !== key),
320
+ assetsError: r.error,
321
+ }));
322
+ }
323
+ },
324
+
325
+ renderVideos: async (projectId) => {
326
+ const result = get().result;
327
+ if (!result || get().videosRendering) return;
328
+
329
+ set({
330
+ videosRendering: true,
331
+ videoStatus: "Queued…",
332
+ videosError: null,
333
+ videoProgress: null,
334
+ });
335
+ const r = await enqueueTask(
336
+ projectId,
337
+ "render-videos",
338
+ "Render scene videos",
339
+ { characters: result.characters, scenes: result.scenes },
340
+ );
341
+ if (!r.ok) set({ videosRendering: false, videosError: r.error });
342
+ },
343
+
344
+ regenerateVideo: async (projectId, slug) => {
345
+ const result = get().result;
346
+ if (!result || get().regeneratingVideos.includes(slug)) return;
347
+ const scene = result.scenes.find((s) => s.slug === slug);
348
+ if (!scene) return;
349
+
350
+ set((s) => ({
351
+ regeneratingVideos: [...s.regeneratingVideos, slug],
352
+ videosError: null,
353
+ }));
354
+ const r = await enqueueTask(
355
+ projectId,
356
+ "regenerate-video",
357
+ `Regenerate video: ${slug}`,
358
+ { slug, scene, characters: result.characters },
359
+ );
360
+ if (!r.ok) {
361
+ set((s) => ({
362
+ regeneratingVideos: s.regeneratingVideos.filter((k) => k !== slug),
363
+ videosError: r.error,
364
+ }));
365
+ }
366
+ },
367
+
368
+ renderSceneImages: async (projectId) => {
369
+ const result = get().result;
370
+ if (!result || get().sceneImagesRendering) return;
371
+
372
+ set({
373
+ sceneImagesRendering: true,
374
+ sceneImageStatus: "Queued…",
375
+ sceneImagesError: null,
376
+ sceneImageProgress: null,
377
+ });
378
+ const r = await enqueueTask(
379
+ projectId,
380
+ "render-scene-images",
381
+ "Render scene images",
382
+ { scenes: result.scenes },
383
+ );
384
+ if (!r.ok) set({ sceneImagesRendering: false, sceneImagesError: r.error });
385
+ },
386
+
387
+ regenerateSceneImage: async (projectId, slug) => {
388
+ const result = get().result;
389
+ if (!result || get().regeneratingSceneImages.includes(slug)) return;
390
+ const scene = result.scenes.find((s) => s.slug === slug);
391
+ if (!scene) return;
392
+
393
+ set((s) => ({
394
+ regeneratingSceneImages: [...s.regeneratingSceneImages, slug],
395
+ sceneImagesError: null,
396
+ }));
397
+ const r = await enqueueTask(
398
+ projectId,
399
+ "regenerate-scene-image",
400
+ `Regenerate scene image: ${slug}`,
401
+ { slug, scene },
402
+ );
403
+ if (!r.ok) {
404
+ set((s) => ({
405
+ regeneratingSceneImages: s.regeneratingSceneImages.filter(
406
+ (k) => k !== slug,
407
+ ),
408
+ sceneImagesError: r.error,
409
+ }));
410
+ }
411
+ },
412
+
413
+ // Reconcile the movie studio store with the latest queue task state.
414
+ applyQueueTask: (task) => {
415
+ const isActive = task.status === "pending" || task.status === "running";
416
+ const err = task.status === "failed" ? task.error : null;
417
+
418
+ switch (task.type) {
419
+ case "generate": {
420
+ if (task.status === "completed" && task.result) {
421
+ if (!appliedCompleted.has(task.id)) {
422
+ appliedCompleted.add(task.id);
423
+ set({ result: task.result, generating: false });
424
+ persistMovieStudioState();
425
+ playDing3x();
426
+ }
427
+ } else if (err) {
428
+ set({ generating: false, error: err });
429
+ } else {
430
+ set({ generating: isActive });
431
+ }
432
+ break;
433
+ }
434
+
435
+ case "render-assets": {
436
+ if (task.status === "completed" && task.result) {
437
+ if (!appliedCompleted.has(task.id)) {
438
+ appliedCompleted.add(task.id);
439
+ set({
440
+ assets: task.result.assets ?? [],
441
+ assetsRendering: false,
442
+ assetStatus: "Assets rendered",
443
+ });
444
+ persistMovieStudioState();
445
+ playDing3x();
446
+ }
447
+ } else if (err) {
448
+ set({ assetsRendering: false, assetsError: err });
449
+ } else {
450
+ set((s) => ({
451
+ assetsRendering: isActive,
452
+ assetStatus: task.status === "running" ? task.statusText : null,
453
+ assetsError: null,
454
+ assets: task.result?.assets ?? s.assets,
455
+ }));
456
+ }
457
+ break;
458
+ }
459
+
460
+ case "render-scene-images": {
461
+ if (task.status === "completed" && task.result) {
462
+ if (!appliedCompleted.has(task.id)) {
463
+ appliedCompleted.add(task.id);
464
+ set({
465
+ sceneImages: task.result.sceneImages ?? [],
466
+ sceneImagesRendering: false,
467
+ sceneImageStatus: "Scene images rendered",
468
+ sceneImageProgress: null,
469
+ });
470
+ persistMovieStudioState();
471
+ playDing3x();
472
+ }
473
+ } else if (err) {
474
+ set({ sceneImagesRendering: false, sceneImagesError: err });
475
+ } else {
476
+ set((s) => ({
477
+ sceneImagesRendering: isActive,
478
+ sceneImageStatus: task.status === "running" ? task.statusText : null,
479
+ sceneImageProgress: task.progress,
480
+ sceneImagesError: null,
481
+ sceneImages: task.result?.sceneImages ?? s.sceneImages,
482
+ }));
483
+ }
484
+ break;
485
+ }
486
+
487
+ case "render-videos": {
488
+ if (task.status === "completed" && task.result) {
489
+ if (!appliedCompleted.has(task.id)) {
490
+ appliedCompleted.add(task.id);
491
+ set({
492
+ videos: task.result.videos ?? [],
493
+ videosRendering: false,
494
+ videoStatus: "Videos rendered",
495
+ videoProgress: null,
496
+ });
497
+ persistMovieStudioState();
498
+ playDing3x();
499
+ }
500
+ } else if (err) {
501
+ set({ videosRendering: false, videosError: err });
502
+ } else {
503
+ set((s) => ({
504
+ videosRendering: isActive,
505
+ videoStatus: task.status === "running" ? task.statusText : null,
506
+ videoProgress: task.progress,
507
+ videosError: null,
508
+ videos: task.result?.videos ?? s.videos,
509
+ }));
510
+ }
511
+ break;
512
+ }
513
+
514
+ case "render": {
515
+ if (task.status === "completed" && task.result) {
516
+ if (!appliedCompleted.has(task.id)) {
517
+ appliedCompleted.add(task.id);
518
+ set({
519
+ renderedScenes: task.result.renderedScenes ?? [],
520
+ assets: task.result.assets ?? get().assets,
521
+ sceneImages: task.result.sceneImages ?? get().sceneImages,
522
+ videos: task.result.videos ?? get().videos,
523
+ rendering: false,
524
+ renderStatus: "Render complete",
525
+ renderProgress: null,
526
+ });
527
+ persistMovieStudioState();
528
+ playDing3x();
529
+ }
530
+ } else if (err) {
531
+ set({ rendering: false, renderError: err });
532
+ } else {
533
+ set((s) => ({
534
+ rendering: isActive,
535
+ renderStatus: task.status === "running" ? task.statusText : null,
536
+ renderProgress: task.progress,
537
+ renderError: null,
538
+ assets: task.result?.assets ?? s.assets,
539
+ sceneImages: task.result?.sceneImages ?? s.sceneImages,
540
+ videos: task.result?.videos ?? s.videos,
541
+ }));
542
+ }
543
+ break;
544
+ }
545
+
546
+ case "regenerate-asset": {
547
+ const key = `${task.payload?.kind}:${task.payload?.slug}`;
548
+ if (task.status === "completed" && task.result) {
549
+ if (!appliedCompleted.has(task.id)) {
550
+ appliedCompleted.add(task.id);
551
+ const r = task.result;
552
+ set((s) => ({
553
+ assets: [
554
+ ...s.assets.filter((a) => `${a.kind}:${a.slug}` !== key),
555
+ r,
556
+ ],
557
+ regenerating: s.regenerating.filter((k) => k !== key),
558
+ }));
559
+ persistMovieStudioState();
560
+ }
561
+ } else if (
562
+ task.status === "failed" ||
563
+ task.status === "cancelled" ||
564
+ task.status === "paused"
565
+ ) {
566
+ set((s) => ({
567
+ regenerating: s.regenerating.filter((k) => k !== key),
568
+ assetsError: err,
569
+ }));
570
+ }
571
+ break;
572
+ }
573
+
574
+ case "regenerate-video": {
575
+ const slug = task.payload?.slug;
576
+ if (task.status === "completed" && task.result) {
577
+ if (!appliedCompleted.has(task.id)) {
578
+ appliedCompleted.add(task.id);
579
+ const r = task.result;
580
+ set((s) => ({
581
+ videos: upsertVideo(s.videos, slug, r),
582
+ regeneratingVideos: s.regeneratingVideos.filter((k) => k !== slug),
583
+ }));
584
+ persistMovieStudioState();
585
+ }
586
+ } else if (
587
+ task.status === "failed" ||
588
+ task.status === "cancelled" ||
589
+ task.status === "paused"
590
+ ) {
591
+ set((s) => ({
592
+ regeneratingVideos: s.regeneratingVideos.filter((k) => k !== slug),
593
+ videosError: err,
594
+ }));
595
+ }
596
+ break;
597
+ }
598
+
599
+ case "regenerate-scene-image": {
600
+ const slug = task.payload?.slug;
601
+ if (task.status === "completed" && task.result) {
602
+ if (!appliedCompleted.has(task.id)) {
603
+ appliedCompleted.add(task.id);
604
+ const r = task.result;
605
+ set((s) => ({
606
+ sceneImages: upsertSceneImage(s.sceneImages, slug, r),
607
+ regeneratingSceneImages: s.regeneratingSceneImages.filter(
608
+ (k) => k !== slug,
609
+ ),
610
+ }));
611
+ persistMovieStudioState();
612
+ }
613
+ } else if (
614
+ task.status === "failed" ||
615
+ task.status === "cancelled" ||
616
+ task.status === "paused"
617
+ ) {
618
+ set((s) => ({
619
+ regeneratingSceneImages: s.regeneratingSceneImages.filter(
620
+ (k) => k !== slug,
621
+ ),
622
+ sceneImagesError: err,
623
+ }));
624
+ }
625
+ break;
626
+ }
627
+ }
628
+ },
629
+
630
+ // Mark already-finished tasks as applied so re-opening a project does not
631
+ // re-apply stale results over newer persisted state.
632
+ primeAppliedQueue: (tasks) => {
633
+ for (const t of tasks) {
634
+ if (t.status === "completed" || t.status === "failed" || t.status === "cancelled") {
635
+ appliedCompleted.add(t.id);
636
+ }
637
+ }
638
+ },
639
+
640
+ stop: () => {
641
+ fetch(`${API_BASE}/api/render/cancel`, { method: "POST" }).catch(() => {});
642
+ const projectId = get().projectId;
643
+ if (projectId) {
644
+ fetch(`${API_BASE}/api/queue/cancel-active`, {
645
+ method: "POST",
646
+ headers: { "Content-Type": "application/json" },
647
+ body: JSON.stringify({ projectId }),
648
+ }).catch(() => {});
649
+ }
650
+ set({
651
+ generating: false,
652
+ rendering: false,
653
+ assetsRendering: false,
654
+ videosRendering: false,
655
+ sceneImagesRendering: false,
656
+ regenerating: [],
657
+ regeneratingVideos: [],
658
+ regeneratingSceneImages: [],
659
+ });
660
+ },
661
+
662
+ reset: () =>
663
+ set({
664
+ idea: "",
665
+ generating: false,
666
+ result: null,
667
+ error: null,
668
+ rendering: false,
669
+ renderStatus: null,
670
+ renderLogs: [],
671
+ renderError: null,
672
+ renderProgress: null,
673
+ assets: [],
674
+ assetsRendering: false,
675
+ assetStatus: null,
676
+ assetsError: null,
677
+ regenerating: [],
678
+ renderedScenes: [],
679
+ videos: [],
680
+ videosRendering: false,
681
+ videoStatus: null,
682
+ videosError: null,
683
+ videoProgress: null,
684
+ regeneratingVideos: [],
685
+ sceneImages: [],
686
+ sceneImagesRendering: false,
687
+ sceneImageStatus: null,
688
+ sceneImagesError: null,
689
+ sceneImageProgress: null,
690
+ regeneratingSceneImages: [],
691
+ }),
692
+ }));
693
+
694
+ function persistMovieStudioState() {
695
+ const s = useMovieStudioStore.getState();
696
+ if (!s.projectId) return;
697
+ fetch(`${API_BASE}/api/movie-studio/state`, {
698
+ method: "POST",
699
+ headers: { "Content-Type": "application/json" },
700
+ body: JSON.stringify({
701
+ projectId: s.projectId,
702
+ idea: s.idea,
703
+ result: s.result,
704
+ assets: s.assets,
705
+ videos: s.videos,
706
+ sceneImages: s.sceneImages,
707
+ }),
708
+ }).catch(() => {});
709
+ }