@effectnode/media 0.9.0 → 0.11.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 (32) hide show
  1. package/dist/backend/movie-backend/agent/prompt/ltx.txt +20 -0
  2. package/dist/backend/movie-backend/agent/prompt/script.md +111 -1
  3. package/dist/backend/movie-backend/core.js +41 -0
  4. package/dist/backend/movie-backend/generation-queue.d.ts +1 -1
  5. package/dist/backend/movie-backend/generation-queue.js +75 -8
  6. package/dist/backend/movie-backend/render-media.d.ts +79 -2
  7. package/dist/backend/movie-backend/render-media.js +694 -418
  8. package/frontend/src/movie-app/components/EditorTabs/AdvancedVoiceCloneTab.tsx +366 -0
  9. package/frontend/src/movie-app/components/EditorTabs/AudioToVideoTab.tsx +406 -0
  10. package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +47 -0
  11. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +155 -1
  12. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +33 -13
  13. package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +26 -5
  14. package/frontend/src/movie-app/components/EditorTabs/UpscaleTab.tsx +343 -0
  15. package/frontend/src/movie-app/components/EditorTabs/VoiceCloneTab.tsx +365 -0
  16. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +130 -125
  17. package/frontend/src/movie-app/stores/advancedVoiceCloneStore.ts +205 -0
  18. package/frontend/src/movie-app/stores/aiModelStore.ts +25 -2
  19. package/frontend/src/movie-app/stores/audioToVideoStore.ts +274 -0
  20. package/frontend/src/movie-app/stores/generationStore.ts +156 -255
  21. package/frontend/src/movie-app/stores/movieStudioStore.ts +10 -3
  22. package/frontend/src/movie-app/stores/queueStore.ts +47 -1
  23. package/frontend/src/movie-app/stores/upscaleStore.ts +118 -0
  24. package/frontend/src/movie-app/stores/voiceCloneStore.ts +227 -0
  25. package/package.json +1 -1
  26. package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +0 -913
  27. package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +0 -305
  28. package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +0 -249
  29. package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +0 -267
  30. package/frontend/src/movie-app/lib/batchVoiceStorage.ts +0 -75
  31. package/frontend/src/movie-app/stores/batchVoiceStore.ts +0 -990
  32. package/frontend/src/movie-app/stores/sceneVisualStore.ts +0 -251
@@ -16,17 +16,17 @@ export type GenerationTab =
16
16
  | "movieStudio"
17
17
  | "fastImageEdit"
18
18
  | "video"
19
- | "extend"
20
19
  | "agent"
21
20
  | "storyWriter"
22
- | "extract"
23
- | "sceneVisual"
24
21
  | "textToImage"
25
22
  | "batchVideo"
26
23
  | "batchImageToVideo"
27
- | "batchVoice"
28
24
  | "llmServer"
29
- | "aiModels";
25
+ | "aiModels"
26
+ | "upscale"
27
+ | "voiceClone"
28
+ | "audioToVideo"
29
+ | "advancedVoiceClone";
30
30
  export type AspectRatio = "1:1" | "16:9" | "9:16" | "4:3" | "3:4";
31
31
  export type Resolution =
32
32
  | "320p"
@@ -77,16 +77,9 @@ interface VideoState {
77
77
  aspectRatio: AspectRatio;
78
78
  resolution: Resolution;
79
79
  mode: VideoMode;
80
- generating: boolean;
81
- result: string | null;
82
- error: string | null;
83
- logs: string[];
84
- }
85
-
86
- interface ExtendState {
87
- prompt: string;
88
- extendDuration: number;
89
- extendFrames: number;
80
+ model: string;
81
+ stage1Steps: number;
82
+ stage2Steps: number;
90
83
  generating: boolean;
91
84
  result: string | null;
92
85
  error: string | null;
@@ -114,6 +107,8 @@ interface TextToImageState {
114
107
 
115
108
  interface FastImageEditState {
116
109
  prompt: string;
110
+ steps: number;
111
+ upscale: string;
117
112
  referenceImages: ProjectImage[];
118
113
  downloading: boolean;
119
114
  downloadingLogs: string[];
@@ -159,18 +154,14 @@ interface GenerationStore {
159
154
  setVideoAspectRatio: (v: AspectRatio) => void;
160
155
  setVideoResolution: (v: Resolution) => void;
161
156
  setVideoMode: (v: VideoMode) => void;
157
+ setVideoModel: (v: string) => void;
158
+ setVideoStage1Steps: (v: number) => void;
159
+ setVideoStage2Steps: (v: number) => void;
162
160
  clearVideoResult: () => void;
163
161
  generateVideo: (projectId: string, imagePath?: string) => Promise<void>;
162
+ applyVideoQueueTask: (task: QueueTask) => void;
164
163
  cancelGenerate: () => void;
165
164
 
166
- // Video extension
167
- extend: ExtendState;
168
- setExtendPrompt: (v: string) => void;
169
- setExtendDuration: (v: number) => void;
170
- setExtendFrames: (v: number) => void;
171
- clearExtendResult: () => void;
172
- generateExtend: (projectId: string, videoPath: string) => Promise<void>;
173
-
174
165
  // Text-to-image (mlx-gen z-image-turbo)
175
166
  textToImage: TextToImageState;
176
167
  setTextToImagePrompt: (v: string) => void;
@@ -193,6 +184,8 @@ interface GenerationStore {
193
184
  // Fast image edit (mlx-gen FLUX.2 Klein)
194
185
  fastImageEdit: FastImageEditState;
195
186
  setFastImageEditPrompt: (v: string) => void;
187
+ setFastImageEditSteps: (v: number) => void;
188
+ setFastImageEditUpscale: (v: string) => void;
196
189
  toggleFastImageEditImage: (img: ProjectImage) => void;
197
190
  clearFastImageEditImages: () => void;
198
191
  clearFastImageEditResult: () => void;
@@ -216,8 +209,6 @@ interface GenerationStore {
216
209
  projectVideos: ProjectVideo[];
217
210
  projectVideosLoading: boolean;
218
211
  fetchProjectVideos: (projectId: string) => Promise<void>;
219
- selectedVideo: ProjectVideo | null;
220
- selectVideo: (video: ProjectVideo | null) => void;
221
212
 
222
213
  // Project audio picker
223
214
  projectAudios: ProjectAudio[];
@@ -393,6 +384,8 @@ async function enqueueFastImageEditTask(
393
384
  projectId: string,
394
385
  prompt: string,
395
386
  images: string[],
387
+ steps: number,
388
+ upscale: string,
396
389
  ): Promise<{ ok: boolean; error?: string; taskId?: string }> {
397
390
  try {
398
391
  const res = await fetch(`${API_BASE}/api/queue/enqueue`, {
@@ -402,7 +395,44 @@ async function enqueueFastImageEditTask(
402
395
  projectId,
403
396
  type: "fast-image-edit",
404
397
  label: "Fast image edit",
405
- payload: { prompt, images, projectId },
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) };
408
+ }
409
+ }
410
+
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,
406
436
  }),
407
437
  });
408
438
  if (!res.ok) {
@@ -428,6 +458,14 @@ let fastImageEditProjectId: string | null = null;
428
458
  /** The queue task id enqueued by the current generate action, if any. */
429
459
  let fastImageEditActiveTaskId: string | null = null;
430
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
+
431
469
  // ========== Beep ==========
432
470
 
433
471
  function playBeep() {
@@ -468,16 +506,9 @@ const initialVideo: VideoState = {
468
506
  aspectRatio: "1:1",
469
507
  resolution: "480p",
470
508
  mode: "distilled",
471
- generating: false,
472
- result: null,
473
- error: null,
474
- logs: [],
475
- };
476
-
477
- const initialExtend: ExtendState = {
478
- prompt: "Continue the scene: the camera holds, motion flows naturally...",
479
- extendDuration: 2,
480
- extendFrames: 2 * 24 + 1,
509
+ model: "dgrauet/ltx-2.3-mlx-q8",
510
+ stage1Steps: 30,
511
+ stage2Steps: 3,
481
512
  generating: false,
482
513
  result: null,
483
514
  error: null,
@@ -530,6 +561,8 @@ const initialTextToImage: TextToImageState = {
530
561
 
531
562
  const initialFastImageEdit: FastImageEditState = {
532
563
  prompt: "",
564
+ steps: 4,
565
+ upscale: "none",
533
566
  referenceImages: [],
534
567
  downloading: false,
535
568
  downloadingLogs: [],
@@ -686,6 +719,21 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
686
719
  setVideoResolution: (resolution) =>
687
720
  set((s) => ({ video: { ...s.video, resolution } })),
688
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
+ })),
689
737
  clearVideoResult: () =>
690
738
  set((s) => ({
691
739
  video: { ...s.video, result: null, error: null, logs: [] },
@@ -735,10 +783,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
735
783
  // The backend now only accepts bare filenames — extract just the basename
736
784
  resolvedImagePath = resolvedImagePath.split("/").pop() || resolvedImagePath;
737
785
 
738
- // Create a fresh AbortController for this single generate run
739
- generateAbortController = new AbortController();
740
- const signal = generateAbortController.signal;
741
-
742
786
  set((s) => ({
743
787
  video: {
744
788
  ...s.video,
@@ -749,75 +793,63 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
749
793
  },
750
794
  }));
751
795
 
752
- try {
753
- const res = await fetch(`${API_BASE}/api/render/image-to-video`, {
754
- method: "POST",
755
- headers: { "Content-Type": "application/json" },
756
- body: JSON.stringify({
757
- prompt: video.prompt.trim(),
758
- imagePath: resolvedImagePath,
759
- projectId,
760
- width,
761
- height,
762
- frames: video.duration * 24 + 1,
763
- frameRate: 24,
764
- mode: video.mode,
765
- }),
766
- signal,
767
- });
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
+ },
768
820
 
769
- if (!res.ok) {
770
- const err = await res.text();
771
- set((s) => ({
772
- video: { ...s.video, generating: false, error: err },
773
- }));
774
- generateAbortController = null;
775
- return;
776
- }
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;
777
828
 
778
- await readSSEStream(res, (event, data) => {
779
- switch (event) {
780
- case "log":
781
- set((s) => ({
782
- video: {
783
- ...s.video,
784
- logs: [...s.video.logs, data.text as string],
785
- },
786
- }));
787
- break;
788
- case "complete":
789
- set((s) => ({
790
- video: {
791
- ...s.video,
792
- generating: false,
793
- result: `http://localhost:${(window as any).PORT}/api/files?path=${encodeURIComponent(data.path)}`,
794
- },
795
- }));
796
- break;
797
- case "error":
798
- set((s) => ({
799
- video: {
800
- ...s.video,
801
- generating: false,
802
- error: data.error || "Video generation failed",
803
- },
804
- }));
805
- break;
806
- }
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
+ },
807
838
  });
808
- } catch (e: any) {
809
- // If the request was aborted, just stop silently
810
- if (e?.name !== "AbortError") {
811
- set((s) => ({
812
- video: { ...s.video, generating: false, error: String(e) },
813
- }));
814
- } else {
815
- set((s) => ({
816
- video: { ...s.video, generating: false },
817
- }));
818
- }
819
- } finally {
820
- generateAbortController = null;
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 } });
821
853
  }
822
854
  },
823
855
 
@@ -1045,6 +1077,9 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
1045
1077
  frames: video.duration * 24 + 1,
1046
1078
  frameRate: 24,
1047
1079
  mode: video.mode,
1080
+ model: video.model,
1081
+ stage1Steps: video.stage1Steps,
1082
+ stage2Steps: video.stage2Steps,
1048
1083
  }),
1049
1084
  signal,
1050
1085
  });
@@ -1112,140 +1147,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
1112
1147
  }
1113
1148
  },
1114
1149
 
1115
- // ---- Video Extension ----
1116
- extend: { ...initialExtend },
1117
-
1118
- setExtendPrompt: (prompt) =>
1119
- set((s) => ({ extend: { ...s.extend, prompt, error: null } })),
1120
- setExtendDuration: (extendDuration) =>
1121
- set((s) => ({
1122
- extend: {
1123
- ...s.extend,
1124
- extendDuration,
1125
- extendFrames: extendDuration * 24 + 1,
1126
- },
1127
- })),
1128
- setExtendFrames: (extendFrames) =>
1129
- set((s) => ({
1130
- extend: {
1131
- ...s.extend,
1132
- extendFrames,
1133
- extendDuration: Math.max(0.5, (extendFrames - 1) / 24),
1134
- },
1135
- })),
1136
- clearExtendResult: () =>
1137
- set((s) => ({
1138
- extend: { ...s.extend, result: null, error: null, logs: [] },
1139
- })),
1140
-
1141
- generateExtend: async (projectId, videoPath) => {
1142
- const { extend } = get();
1143
- if (!extend.prompt.trim() || extend.generating) return;
1144
-
1145
- // Extract raw file path from HTTP URL (e.g. http://localhost:PORT/api/files?path=...)
1146
- let resolvedVideoPath = videoPath || "";
1147
- if (!resolvedVideoPath) {
1148
- set((s) => ({
1149
- extend: { ...s.extend, error: "Select a video to extend first." },
1150
- }));
1151
- return;
1152
- }
1153
- if (resolvedVideoPath.includes("/api/files?path=")) {
1154
- try {
1155
- const url = new URL(resolvedVideoPath);
1156
- resolvedVideoPath = url.searchParams.get("path") || resolvedVideoPath;
1157
- } catch {
1158
- // not a valid URL, use as-is
1159
- }
1160
- }
1161
- if (resolvedVideoPath.startsWith("file://")) {
1162
- resolvedVideoPath = resolvedVideoPath.slice(7);
1163
- }
1164
- // The backend only accepts bare filenames — extract just the basename
1165
- resolvedVideoPath = resolvedVideoPath.split("/").pop() || resolvedVideoPath;
1166
-
1167
- // Create a fresh AbortController for this extend run
1168
- generateAbortController = new AbortController();
1169
- const signal = generateAbortController.signal;
1170
-
1171
- set((s) => ({
1172
- extend: {
1173
- ...s.extend,
1174
- generating: true,
1175
- error: null,
1176
- result: null,
1177
- logs: [],
1178
- },
1179
- }));
1180
-
1181
- try {
1182
- const res = await fetch(`${API_BASE}/api/render/extend-video`, {
1183
- method: "POST",
1184
- headers: { "Content-Type": "application/json" },
1185
- body: JSON.stringify({
1186
- prompt: extend.prompt.trim(),
1187
- videoPath: resolvedVideoPath,
1188
- projectId,
1189
- extendFrames: extend.extendDuration * 24 + 1,
1190
- }),
1191
- signal,
1192
- });
1193
-
1194
- if (!res.ok) {
1195
- const err = await res.text();
1196
- set((s) => ({
1197
- extend: { ...s.extend, generating: false, error: err },
1198
- }));
1199
- generateAbortController = null;
1200
- return;
1201
- }
1202
-
1203
- await readSSEStream(res, (event, data) => {
1204
- switch (event) {
1205
- case "log":
1206
- set((s) => ({
1207
- extend: {
1208
- ...s.extend,
1209
- logs: [...s.extend.logs, data.text as string],
1210
- },
1211
- }));
1212
- break;
1213
- case "complete":
1214
- set((s) => ({
1215
- extend: {
1216
- ...s.extend,
1217
- generating: false,
1218
- result: `http://localhost:${(window as any).PORT}/api/files?path=${encodeURIComponent(data.path)}`,
1219
- },
1220
- }));
1221
- // Refresh the video grid so the new extended-*.mp4 appears
1222
- get().fetchProjectVideos(projectId);
1223
- break;
1224
- case "error":
1225
- set((s) => ({
1226
- extend: {
1227
- ...s.extend,
1228
- generating: false,
1229
- error: data.error || "Video extension failed",
1230
- },
1231
- }));
1232
- break;
1233
- }
1234
- });
1235
- } catch (e: any) {
1236
- // If the request was aborted, just stop silently
1237
- if (e?.name !== "AbortError") {
1238
- set((s) => ({
1239
- extend: { ...s.extend, generating: false, error: String(e) },
1240
- }));
1241
- } else {
1242
- set((s) => ({ extend: { ...s.extend, generating: false } }));
1243
- }
1244
- } finally {
1245
- generateAbortController = null;
1246
- }
1247
- },
1248
-
1249
1150
  // ---- Fast Image Edit (FLUX.2 Klein) ----
1250
1151
  fastImageEdit: { ...initialFastImageEdit },
1251
1152
 
@@ -1254,6 +1155,17 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
1254
1155
  fastImageEdit: { ...s.fastImageEdit, prompt, error: null },
1255
1156
  })),
1256
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
+
1257
1169
  toggleFastImageEditImage: (img) =>
1258
1170
  set((s) => {
1259
1171
  const current = s.fastImageEdit.referenceImages;
@@ -1420,6 +1332,8 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
1420
1332
  projectId,
1421
1333
  fastImageEdit.prompt.trim(),
1422
1334
  images,
1335
+ fastImageEdit.steps,
1336
+ fastImageEdit.upscale,
1423
1337
  );
1424
1338
  fastImageEditActiveTaskId = r.taskId ?? null;
1425
1339
  if (!r.ok) {
@@ -2006,7 +1920,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
2006
1920
  // ---- Project Videos ----
2007
1921
  projectVideos: [],
2008
1922
  projectVideosLoading: false,
2009
- selectedVideo: null,
2010
1923
 
2011
1924
  fetchProjectVideos: async (projectId) => {
2012
1925
  set({ projectVideosLoading: true });
@@ -2027,17 +1940,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
2027
1940
  }
2028
1941
  },
2029
1942
 
2030
- selectVideo: (video) => {
2031
- if (!video) {
2032
- set({ selectedVideo: null });
2033
- return;
2034
- }
2035
- const fullUrl = video.url.startsWith("http")
2036
- ? video.url
2037
- : `http://localhost:${(window as any).PORT}${video.url}`;
2038
- set({ selectedVideo: { ...video, url: fullUrl } });
2039
- },
2040
-
2041
1943
  // ---- Project Audio ----
2042
1944
  projectAudios: [],
2043
1945
  projectAudiosLoading: false,
@@ -2068,7 +1970,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
2068
1970
  // Immediately reset generating state so UI returns to ready
2069
1971
  set((s) => ({
2070
1972
  video: { ...s.video, generating: false },
2071
- extend: { ...s.extend, generating: false },
2072
1973
  textToImage: { ...s.textToImage, generating: false },
2073
1974
  }));
2074
1975
  // Also kill the backend spawn process
@@ -2109,11 +2010,12 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
2109
2010
  resetAll: () => {
2110
2011
  fastImageEditProjectId = null;
2111
2012
  fastImageEditActiveTaskId = null;
2013
+ videoProjectId = null;
2014
+ videoActiveTaskId = null;
2112
2015
  set({
2113
2016
  activeTab: "movieStudio",
2114
2017
  image: { ...initialImage },
2115
2018
  video: { ...initialVideo },
2116
- extend: { ...initialExtend },
2117
2019
  fastImageEdit: { ...initialFastImageEdit },
2118
2020
  textToImage: { ...initialTextToImage },
2119
2021
  textToImageProjectId: null,
@@ -2128,7 +2030,6 @@ export const useGenerationStore = create<GenerationStore>((set, get) => ({
2128
2030
  selectedImage: null,
2129
2031
  projectVideos: [],
2130
2032
  projectVideosLoading: false,
2131
- selectedVideo: null,
2132
2033
  projectAudios: [],
2133
2034
  projectAudiosLoading: false,
2134
2035
  csvRows: [],
@@ -274,7 +274,9 @@ interface MovieStudioStore {
274
274
  sceneImagesError: string | null;
275
275
  sceneImageProgress: { current: number; total: number } | null;
276
276
  regeneratingSceneImages: string[];
277
+ sceneImageSteps: number;
277
278
  setIdea: (v: string) => void;
279
+ setSceneImageSteps: (v: number) => void;
278
280
  hydrate: (projectId: string) => Promise<void>;
279
281
  generate: (projectId: string, model: string) => Promise<void>;
280
282
  render: (projectId: string) => Promise<void>;
@@ -330,12 +332,16 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
330
332
  sceneImagesError: null,
331
333
  sceneImageProgress: null,
332
334
  regeneratingSceneImages: [],
335
+ sceneImageSteps: 6,
333
336
 
334
337
  setIdea: (idea) => {
335
338
  set({ idea, error: null });
336
339
  persistMovieStudioState();
337
340
  },
338
341
 
342
+ setSceneImageSteps: (steps) =>
343
+ set({ sceneImageSteps: Math.max(1, Math.round(Number(steps)) || 1) }),
344
+
339
345
  hydrate: async (projectId) => {
340
346
  // Switching projects: reset to defaults so the previous project's idea
341
347
  // doesn't leak through, then load the stored state (if any) below.
@@ -435,7 +441,7 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
435
441
  projectId,
436
442
  "render-scene-image",
437
443
  `Render scene image: ${scene.slug}`,
438
- { scene, batchId },
444
+ { scene, batchId, steps: get().sceneImageSteps },
439
445
  );
440
446
  if (!r.ok) {
441
447
  const b = batches.get(batchId);
@@ -611,7 +617,7 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
611
617
  projectId,
612
618
  "render-scene-image",
613
619
  `Render scene image: ${scene.slug}`,
614
- { scene, batchId },
620
+ { scene, batchId, steps: get().sceneImageSteps },
615
621
  );
616
622
  if (!r.ok) {
617
623
  const b = batches.get(batchId);
@@ -637,7 +643,7 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
637
643
  projectId,
638
644
  "regenerate-scene-image",
639
645
  `Regenerate scene image: ${slug}`,
640
- { slug, scene },
646
+ { slug, scene, steps: get().sceneImageSteps },
641
647
  );
642
648
  if (!r.ok) {
643
649
  set((s) => ({
@@ -1058,6 +1064,7 @@ export const useMovieStudioStore = create<MovieStudioStore>((set, get) => ({
1058
1064
  sceneImagesError: null,
1059
1065
  sceneImageProgress: null,
1060
1066
  regeneratingSceneImages: [],
1067
+ sceneImageSteps: 6,
1061
1068
  });
1062
1069
  },
1063
1070
  }));