@effectnode/media 0.4.0 → 0.6.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 (24) hide show
  1. package/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
  2. package/dist/backend/movie-backend/agent/agent-backend.js +169 -83
  3. package/dist/backend/movie-backend/agent/tools/index.js +0 -2
  4. package/dist/backend/movie-backend/core.js +1 -1
  5. package/dist/backend/movie-backend/generation-queue.js +107 -44
  6. package/dist/backend/movie-backend/render-media.js +28 -254
  7. package/frontend/index.html +6 -1
  8. package/frontend/public/lambobo.png +0 -0
  9. package/frontend/src/movie-app/MediaStudio.tsx +6 -2
  10. package/frontend/src/movie-app/SetupPage.tsx +98 -63
  11. package/frontend/src/movie-app/components/Aurora.tsx +13 -0
  12. package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +5 -8
  13. package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +417 -205
  14. package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +350 -0
  15. package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -67
  16. package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
  17. package/frontend/src/movie-app/index.css +275 -8
  18. package/frontend/src/movie-app/index.html +1 -1
  19. package/frontend/src/movie-app/stores/aiModelStore.ts +172 -0
  20. package/frontend/src/movie-app/stores/generationStore.ts +2 -3
  21. package/frontend/src/movie-app/stores/movieStudioStore.ts +72 -3
  22. package/package.json +2 -2
  23. package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
  24. package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +0 -881
@@ -3,7 +3,7 @@ import { useMovieStudioStore } from "../../stores/movieStudioStore";
3
3
  import { useGenerationStore } from "../../stores/generationStore";
4
4
  import { useProjectStore } from "../../stores/projectStore";
5
5
  import { useQueueStore } from "../../stores/queueStore";
6
- import { OnlyStartButton } from "./MlxVlmServerPanel";
6
+ import MlxVlmServerPanel, { OnlyStartButton } from "./MlxVlmServerPanel";
7
7
  import TaskQueuePanel from "./TaskQueuePanel";
8
8
  import TerminalLogPanel from "./TerminalLogPanel";
9
9
 
@@ -56,7 +56,6 @@ export default function MovieStudioTab({ projectId }: Props) {
56
56
  projectId,
57
57
  store.assets.length,
58
58
  store.sceneImages.length,
59
- store.videos.length,
60
59
  store.renderedScenes.length,
61
60
  ]);
62
61
 
@@ -192,12 +191,6 @@ export default function MovieStudioTab({ projectId }: Props) {
192
191
  </thead>
193
192
  );
194
193
 
195
- const PromptCell = ({ text }: { text: string }) => (
196
- <span className="text-ink-700 whitespace-pre-wrap leading-relaxed">
197
- {text}
198
- </span>
199
- );
200
-
201
194
  const slugify = (v: string): string =>
202
195
  (v || "").trim().replace(/[^a-zA-Z0-9_-]/g, "_");
203
196
  const resolveUrl = (url?: string): string | null =>
@@ -237,6 +230,63 @@ export default function MovieStudioTab({ projectId }: Props) {
237
230
  <span className="text-ink-300 text-xs">—</span>
238
231
  );
239
232
 
233
+ const EditableInput = ({
234
+ value,
235
+ onChange,
236
+ type = "text",
237
+ className = "",
238
+ }: {
239
+ value: string;
240
+ onChange: (v: string) => void;
241
+ type?: string;
242
+ className?: string;
243
+ }) => (
244
+ <input
245
+ type={type}
246
+ value={value}
247
+ onChange={(e) => onChange(e.target.value)}
248
+ className={`w-full rounded-lg border border-ink-200 bg-ink-50 px-2 py-1.5 text-xs text-ink-800 placeholder-ink-400 transition-all focus:border-tiffany-500 focus:outline-none focus:ring-2 focus:ring-tiffany-500/25 ${className}`}
249
+ />
250
+ );
251
+
252
+ const EditableTextarea = ({
253
+ value,
254
+ onChange,
255
+ rows = 3,
256
+ mono = false,
257
+ }: {
258
+ value: string;
259
+ onChange: (v: string) => void;
260
+ rows?: number;
261
+ mono?: boolean;
262
+ }) => (
263
+ <textarea
264
+ value={value}
265
+ onChange={(e) => onChange(e.target.value)}
266
+ rows={rows}
267
+ className={`w-full min-w-[150px] resize-y rounded-lg border border-ink-200 bg-ink-50 px-2 py-1.5 leading-relaxed text-ink-800 placeholder-ink-400 transition-all focus:border-tiffany-500 focus:outline-none focus:ring-2 focus:ring-tiffany-500/25 ${
268
+ mono ? "font-mono text-[11px]" : "text-xs"
269
+ }`}
270
+ />
271
+ );
272
+
273
+ const RegenerateButton = ({
274
+ onClick,
275
+ spinning,
276
+ }: {
277
+ onClick: () => void;
278
+ spinning: boolean;
279
+ }) => (
280
+ <button
281
+ onClick={onClick}
282
+ disabled={spinning}
283
+ className="mt-1 flex items-center gap-1 whitespace-nowrap rounded-md border border-ink-200 px-2 py-1 text-[10px] font-medium text-ink-600 transition-colors hover:border-tiffany-400 hover:text-tiffany-600 disabled:opacity-50"
284
+ >
285
+ {spinning ? SpinnerIcon : RefreshIcon}
286
+ Regenerate
287
+ </button>
288
+ );
289
+
240
290
  return (
241
291
  <div className="flex flex-col gap-7">
242
292
  <div className="flex items-center gap-2">
@@ -274,20 +324,38 @@ export default function MovieStudioTab({ projectId }: Props) {
274
324
 
275
325
  {/* ===== Submit ===== */}
276
326
  {store.generating ? (
277
- <div className="flex items-center gap-3">
278
- <div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
279
- {SpinnerIcon}
280
- <span className="text-sm font-medium text-ink-700">
281
- Generating production bible...
282
- </span>
327
+ <div className="flex flex-col gap-2">
328
+ <div className="flex items-center gap-3">
329
+ <div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
330
+ {SpinnerIcon}
331
+ <span className="text-sm font-medium text-ink-700">
332
+ {store.generateStatus ?? "Generating production bible..."}
333
+ </span>
334
+ </div>
335
+ <button
336
+ onClick={() => store.stop()}
337
+ className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500 hover:bg-red-600 active:bg-red-700 text-white text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm"
338
+ >
339
+ {StopIcon}
340
+ Stop
341
+ </button>
283
342
  </div>
284
- <button
285
- onClick={() => store.stop()}
286
- className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500 hover:bg-red-600 active:bg-red-700 text-white text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm"
287
- >
288
- {StopIcon}
289
- Stop
290
- </button>
343
+ {store.generateProgress && store.generateProgress.total > 0 && (
344
+ <div className="flex items-center gap-3 px-1">
345
+ <div className="flex-1 h-2 bg-ink-200 rounded-full overflow-hidden">
346
+ <div
347
+ className="h-full bg-tiffany-500 rounded-full transition-all duration-300"
348
+ style={{
349
+ width: `${(store.generateProgress.current / store.generateProgress.total) * 100}%`,
350
+ }}
351
+ />
352
+ </div>
353
+ <span className="text-xs font-semibold text-ink-700 tabular-nums whitespace-nowrap">
354
+ {store.generateProgress.current}/
355
+ {store.generateProgress.total}
356
+ </span>
357
+ </div>
358
+ )}
291
359
  </div>
292
360
  ) : (
293
361
  <button
@@ -331,25 +399,65 @@ export default function MovieStudioTab({ projectId }: Props) {
331
399
  ]}
332
400
  />
333
401
  <tbody>
334
- {store.result.characters.map((c) => (
335
- <tr key={c.slug} className="border-b border-ink-200">
336
- <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
337
- {c.slug}
338
- </td>
339
- <td className="border border-ink-200 px-2 py-1.5 align-top font-medium text-ink-800">
340
- {c.name}
341
- </td>
342
- <td className="border border-ink-200 px-2 py-1.5 align-middle">
343
- <Thumb
344
- url={imageUrlFor("character", c.slug)}
345
- filename={c.slug}
346
- />
347
- </td>
348
- <td className="border border-ink-200 px-2 py-1.5 align-top">
349
- <PromptCell text={c.imagePrompt} />
350
- </td>
351
- </tr>
352
- ))}
402
+ {store.result.characters.map((c) => {
403
+ const url = imageUrlFor("character", c.slug);
404
+ const spinning = store.regenerating.includes(
405
+ `character:${c.slug}`,
406
+ );
407
+ return (
408
+ <tr
409
+ key={c.slug}
410
+ className="border-b border-ink-200"
411
+ >
412
+ <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
413
+ {c.slug}
414
+ </td>
415
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[140px]">
416
+ <EditableInput
417
+ value={c.name}
418
+ onChange={(v) =>
419
+ store.updateCharacter(c.slug, { name: v })
420
+ }
421
+ />
422
+ </td>
423
+ <td className="border border-ink-200 px-2 py-1.5 align-top">
424
+ {url ? (
425
+ <div className="flex flex-col items-start">
426
+ <div className="h-16 w-16">
427
+ <Thumb url={url} filename={c.slug} />
428
+ </div>
429
+ <RegenerateButton
430
+ spinning={spinning}
431
+ onClick={() =>
432
+ store.regenerateAsset(
433
+ projectId,
434
+ "character",
435
+ c.slug,
436
+ c.imagePrompt,
437
+ )
438
+ }
439
+ />
440
+ </div>
441
+ ) : (
442
+ <span className="text-ink-300 text-xs">
443
+
444
+ </span>
445
+ )}
446
+ </td>
447
+ <td className="border border-ink-200 px-2 py-1.5 align-top">
448
+ <EditableTextarea
449
+ value={c.imagePrompt}
450
+ onChange={(v) =>
451
+ store.updateCharacter(c.slug, {
452
+ imagePrompt: v,
453
+ })
454
+ }
455
+ rows={3}
456
+ />
457
+ </td>
458
+ </tr>
459
+ );
460
+ })}
353
461
  </tbody>
354
462
  </table>
355
463
  </div>
@@ -377,25 +485,65 @@ export default function MovieStudioTab({ projectId }: Props) {
377
485
  ]}
378
486
  />
379
487
  <tbody>
380
- {store.result.places.map((p) => (
381
- <tr key={p.slug} className="border-b border-ink-200">
382
- <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
383
- {p.slug}
384
- </td>
385
- <td className="border border-ink-200 px-2 py-1.5 align-top font-medium text-ink-800">
386
- {p.name}
387
- </td>
388
- <td className="border border-ink-200 px-2 py-1.5 align-middle">
389
- <Thumb
390
- url={imageUrlFor("place", p.slug)}
391
- filename={p.slug}
392
- />
393
- </td>
394
- <td className="border border-ink-200 px-2 py-1.5 align-top">
395
- <PromptCell text={p.imagePrompt} />
396
- </td>
397
- </tr>
398
- ))}
488
+ {store.result.places.map((p) => {
489
+ const url = imageUrlFor("place", p.slug);
490
+ const spinning = store.regenerating.includes(
491
+ `place:${p.slug}`,
492
+ );
493
+ return (
494
+ <tr
495
+ key={p.slug}
496
+ className="border-b border-ink-200"
497
+ >
498
+ <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
499
+ {p.slug}
500
+ </td>
501
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[140px]">
502
+ <EditableInput
503
+ value={p.name}
504
+ onChange={(v) =>
505
+ store.updatePlace(p.slug, { name: v })
506
+ }
507
+ />
508
+ </td>
509
+ <td className="border border-ink-200 px-2 py-1.5 align-top">
510
+ {url ? (
511
+ <div className="flex flex-col items-start">
512
+ <div className="h-16 w-16">
513
+ <Thumb url={url} filename={p.slug} />
514
+ </div>
515
+ <RegenerateButton
516
+ spinning={spinning}
517
+ onClick={() =>
518
+ store.regenerateAsset(
519
+ projectId,
520
+ "place",
521
+ p.slug,
522
+ p.imagePrompt,
523
+ )
524
+ }
525
+ />
526
+ </div>
527
+ ) : (
528
+ <span className="text-ink-300 text-xs">
529
+
530
+ </span>
531
+ )}
532
+ </td>
533
+ <td className="border border-ink-200 px-2 py-1.5 align-top">
534
+ <EditableTextarea
535
+ value={p.imagePrompt}
536
+ onChange={(v) =>
537
+ store.updatePlace(p.slug, {
538
+ imagePrompt: v,
539
+ })
540
+ }
541
+ rows={3}
542
+ />
543
+ </td>
544
+ </tr>
545
+ );
546
+ })}
399
547
  </tbody>
400
548
  </table>
401
549
  </div>
@@ -404,9 +552,47 @@ export default function MovieStudioTab({ projectId }: Props) {
404
552
 
405
553
  {/* Scenes */}
406
554
  <div>
407
- <h3 className="text-sm font-semibold text-ink-900 mb-2">
408
- Scenes
409
- </h3>
555
+ <div className="flex items-center justify-between mb-2">
556
+ <h3 className="text-sm font-semibold text-ink-900">Scenes</h3>
557
+ {store.videosRendering ? (
558
+ <span className="flex items-center gap-1.5 text-xs text-tiffany-600">
559
+ {SpinnerIcon}
560
+ {store.videoStatus ?? "Rendering..."}
561
+ </span>
562
+ ) : (
563
+ <button
564
+ onClick={() => store.renderVideos(projectId)}
565
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
566
+ >
567
+ {SparkleIcon}
568
+ Generate All Videos
569
+ </button>
570
+ )}
571
+ </div>
572
+
573
+ {store.videosRendering &&
574
+ store.videoProgress &&
575
+ store.videoProgress.total > 0 && (
576
+ <div className="flex items-center gap-3 mb-2 px-1">
577
+ <div className="flex-1 h-2 bg-ink-200 rounded-full overflow-hidden">
578
+ <div
579
+ className="h-full bg-tiffany-500 rounded-full transition-all duration-300"
580
+ style={{
581
+ width: `${(store.videoProgress.current / store.videoProgress.total) * 100}%`,
582
+ }}
583
+ />
584
+ </div>
585
+ <span className="text-xs font-semibold text-ink-700 tabular-nums whitespace-nowrap">
586
+ {store.videoProgress.current}/{store.videoProgress.total}
587
+ </span>
588
+ </div>
589
+ )}
590
+
591
+ {store.videosError && (
592
+ <div className="mb-2 p-3 bg-red-50 border border-red-200 rounded-xl text-red-600 text-xs">
593
+ {store.videosError}
594
+ </div>
595
+ )}
410
596
  {store.result.scenes.length === 0 ? (
411
597
  <p className="text-xs text-ink-500 italic py-3 border border-dashed border-ink-200 rounded-2xl text-center">
412
598
  No scenes found.
@@ -434,6 +620,7 @@ export default function MovieStudioTab({ projectId }: Props) {
434
620
  },
435
621
  { key: "place", label: "Place", className: "w-24" },
436
622
  { key: "image", label: "Image", className: "w-16" },
623
+ { key: "video", label: "Video", className: "w-40" },
437
624
  { key: "script", label: "Script", className: "w-60" },
438
625
  {
439
626
  key: "voiceover",
@@ -444,57 +631,172 @@ export default function MovieStudioTab({ projectId }: Props) {
444
631
  ]}
445
632
  />
446
633
  <tbody>
447
- {store.result.scenes.map((s) => (
448
- <tr key={s.slug} className="border-b border-ink-200">
449
- <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
450
- {s.slug}
451
- </td>
452
- <td className="border border-ink-200 px-2 py-1.5 align-top text-ink-800 tabular-nums whitespace-nowrap">
453
- {s.duration > 0 ? `${s.duration}s` : "—"}
454
- </td>
455
- <td className="border border-ink-200 px-2 py-1.5 align-top text-ink-800 min-w-[150px]">
456
- {s.description}
457
- </td>
458
- <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-ink-600 min-w-[150px]">
459
- {s.characterSlugs.join(", ")}
460
- </td>
461
- <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-ink-600 min-w-[150px]">
462
- {s.placeSlug}
463
- </td>
464
- <td className="border border-ink-200 px-2 py-1.5 align-middle min-w-[200px]">
465
- <Thumb
466
- url={imageUrlFor("scene", s.slug)}
467
- filename={s.slug}
468
- />
469
- </td>
470
- <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
471
- {s.scriptLines.length === 0 ? (
472
- <span className="text-ink-400">—</span>
473
- ) : (
474
- <div className="flex flex-col gap-1">
475
- {s.scriptLines.map((line, i) => (
476
- <div key={i} className="text-ink-700">
477
- <span className="font-mono text-[11px] font-medium text-tiffany-700">
478
- {line.characterSlug}:
479
- </span>{" "}
480
- {line.line}
634
+ {store.result.scenes.map((s) => {
635
+ const url = imageUrlFor("scene", s.slug);
636
+ const spinning =
637
+ store.regeneratingSceneImages.includes(s.slug);
638
+ const video = store.videos.find(
639
+ (v) => v.slug === s.slug,
640
+ );
641
+ const videoUrl = resolveUrl(video?.url);
642
+ const videoSpinning =
643
+ store.regeneratingVideos.includes(s.slug);
644
+ return (
645
+ <tr
646
+ key={s.slug}
647
+ className="border-b border-ink-200"
648
+ >
649
+ <td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
650
+ {s.slug}
651
+ </td>
652
+ <td className="border border-ink-200 px-2 py-1.5 align-top">
653
+ <EditableInput
654
+ type="number"
655
+ value={
656
+ s.duration > 0 ? String(s.duration) : ""
657
+ }
658
+ onChange={(v) =>
659
+ store.updateScene(s.slug, {
660
+ duration: Number(v) || 0,
661
+ })
662
+ }
663
+ className="w-20"
664
+ />
665
+ </td>
666
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[150px]">
667
+ <EditableTextarea
668
+ value={s.description}
669
+ onChange={(v) =>
670
+ store.updateScene(s.slug, {
671
+ description: v,
672
+ })
673
+ }
674
+ rows={3}
675
+ />
676
+ </td>
677
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[150px]">
678
+ <EditableTextarea
679
+ mono
680
+ value={s.characterSlugs.join(", ")}
681
+ onChange={(v) =>
682
+ store.updateScene(s.slug, {
683
+ characterSlugs: v
684
+ .split(",")
685
+ .map((x) => x.trim())
686
+ .filter(Boolean),
687
+ })
688
+ }
689
+ rows={2}
690
+ />
691
+ </td>
692
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[150px]">
693
+ <EditableInput
694
+ value={s.placeSlug}
695
+ onChange={(v) =>
696
+ store.updateScene(s.slug, { placeSlug: v })
697
+ }
698
+ className="font-mono text-[11px]"
699
+ />
700
+ </td>
701
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[200px]">
702
+ {url ? (
703
+ <div className="flex flex-col items-start">
704
+ <div className="h-16 w-16">
705
+ <Thumb url={url} filename={s.slug} />
481
706
  </div>
482
- ))}
483
- </div>
484
- )}
485
- </td>
486
- <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
487
- {s.voiceOver ? (
488
- <PromptCell text={s.voiceOver} />
489
- ) : (
490
- <span className="text-ink-400">—</span>
491
- )}
492
- </td>
493
- <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
494
- <PromptCell text={s.imagePrompt} />
495
- </td>
496
- </tr>
497
- ))}
707
+ <RegenerateButton
708
+ spinning={spinning}
709
+ onClick={() =>
710
+ store.regenerateSceneImage(
711
+ projectId,
712
+ s.slug,
713
+ )
714
+ }
715
+ />
716
+ </div>
717
+ ) : (
718
+ <span className="text-ink-300 text-xs">
719
+
720
+ </span>
721
+ )}
722
+ </td>
723
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[180px]">
724
+ {videoUrl ? (
725
+ <div className="flex flex-col items-start">
726
+ <video
727
+ src={`${videoUrl}&t=${video?.updatedAt}`}
728
+ controls
729
+ className="w-40 rounded-lg border border-ink-200 cursor-pointer"
730
+ onClick={() =>
731
+ openPreview(videoUrl, s.slug, "video")
732
+ }
733
+ />
734
+ <RegenerateButton
735
+ spinning={videoSpinning}
736
+ onClick={() =>
737
+ store.regenerateVideo(projectId, s.slug)
738
+ }
739
+ />
740
+ </div>
741
+ ) : (
742
+ <span className="text-ink-300 text-xs">—</span>
743
+ )}
744
+ </td>
745
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
746
+ <EditableTextarea
747
+ mono
748
+ value={s.scriptLines
749
+ .map((l) => `${l.characterSlug}: ${l.line}`)
750
+ .join("\n")}
751
+ onChange={(v) =>
752
+ store.updateScene(s.slug, {
753
+ scriptLines: v
754
+ .split("\n")
755
+ .map((ln) => {
756
+ const i = ln.indexOf(":");
757
+ return i === -1
758
+ ? {
759
+ characterSlug: "",
760
+ line: ln.trim(),
761
+ }
762
+ : {
763
+ characterSlug: ln
764
+ .slice(0, i)
765
+ .trim(),
766
+ line: ln.slice(i + 1).trim(),
767
+ };
768
+ })
769
+ .filter((l) =>
770
+ Boolean(l.characterSlug || l.line),
771
+ ),
772
+ })
773
+ }
774
+ rows={4}
775
+ />
776
+ </td>
777
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
778
+ <EditableTextarea
779
+ value={s.voiceOver}
780
+ onChange={(v) =>
781
+ store.updateScene(s.slug, { voiceOver: v })
782
+ }
783
+ rows={3}
784
+ />
785
+ </td>
786
+ <td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
787
+ <EditableTextarea
788
+ value={s.imagePrompt}
789
+ onChange={(v) =>
790
+ store.updateScene(s.slug, {
791
+ imagePrompt: v,
792
+ })
793
+ }
794
+ rows={3}
795
+ />
796
+ </td>
797
+ </tr>
798
+ );
799
+ })}
498
800
  </tbody>
499
801
  </table>
500
802
  </div>
@@ -665,7 +967,7 @@ export default function MovieStudioTab({ projectId }: Props) {
665
967
  onClick={() =>
666
968
  openPreview(fullUrl, img.slug, "image")
667
969
  }
668
- className="aspect-[9/16] object-cover object-center w-full cursor-zoom-in"
970
+ className="aspect-square object-cover object-center w-full cursor-zoom-in"
669
971
  />
670
972
  </div>
671
973
  <div className="flex items-center justify-between gap-1">
@@ -691,96 +993,6 @@ export default function MovieStudioTab({ projectId }: Props) {
691
993
  </div>
692
994
  )}
693
995
 
694
- {/* ===== Videos ===== */}
695
- {store.result && (
696
- <div className="flex flex-col gap-3">
697
- <div className="flex items-center justify-between gap-3">
698
- <h3 className="text-sm font-semibold text-ink-900">Videos</h3>
699
- {store.videosRendering ? (
700
- <span className="flex items-center gap-1.5 text-xs text-tiffany-600">
701
- {SpinnerIcon}
702
- {store.videoStatus ?? "Rendering..."}
703
- </span>
704
- ) : (
705
- <button
706
- onClick={() => store.renderVideos(projectId)}
707
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
708
- >
709
- {SparkleIcon}
710
- Render Videos
711
- </button>
712
- )}
713
- </div>
714
-
715
- {store.videosRendering &&
716
- store.videoProgress &&
717
- store.videoProgress.total > 0 && (
718
- <div className="flex items-center gap-3 px-1">
719
- <div className="flex-1 h-2 bg-ink-200 rounded-full overflow-hidden">
720
- <div
721
- className="h-full bg-tiffany-500 rounded-full transition-all duration-300"
722
- style={{
723
- width: `${(store.videoProgress.current / store.videoProgress.total) * 100}%`,
724
- }}
725
- />
726
- </div>
727
- <span className="text-xs font-semibold text-ink-700 tabular-nums whitespace-nowrap">
728
- {store.videoProgress.current}/{store.videoProgress.total}
729
- </span>
730
- </div>
731
- )}
732
-
733
- {store.videosError && (
734
- <div className="p-4 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-xs">
735
- {store.videosError}
736
- </div>
737
- )}
738
-
739
- {store.videos.length > 0 && (
740
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
741
- {store.videos.map((video) => {
742
- const isRegenerating = store.regeneratingVideos.includes(
743
- video.slug,
744
- );
745
- const fullUrl = video.url.startsWith("http")
746
- ? video.url
747
- : `http://localhost:${(window as any).PORT}${video.url}`;
748
- return (
749
- <div
750
- key={video.slug}
751
- className="flex flex-col gap-1.5 border border-ink-200 rounded-xl p-2 bg-white"
752
- >
753
- <video
754
- src={`${fullUrl}&t=${video.updatedAt}`}
755
- controls
756
- onClick={() =>
757
- openPreview(fullUrl, video.slug, "video")
758
- }
759
- className="w-full rounded-lg border border-ink-200 cursor-pointer"
760
- />
761
- <div className="flex items-center justify-between gap-1">
762
- <span className="text-[11px] font-mono text-ink-600 truncate">
763
- {video.slug}
764
- </span>
765
- <button
766
- onClick={() =>
767
- store.regenerateVideo(projectId, video.slug)
768
- }
769
- disabled={isRegenerating}
770
- className="flex items-center gap-1 px-2 py-1 text-[11px] font-medium rounded-lg border border-ink-200 text-ink-600 hover:border-tiffany-400 hover:text-tiffany-600 transition-colors disabled:opacity-50"
771
- >
772
- {isRegenerating ? SpinnerIcon : RefreshIcon}
773
- Regenerate
774
- </button>
775
- </div>
776
- </div>
777
- );
778
- })}
779
- </div>
780
- )}
781
- </div>
782
- )}
783
-
784
996
  {/* ===== Render ===== */}
785
997
  {store.result && (
786
998
  <div className="flex flex-col gap-3">
@@ -885,8 +1097,8 @@ export default function MovieStudioTab({ projectId }: Props) {
885
1097
  </>
886
1098
  ) : (
887
1099
  <>
888
- <OnlyStartButton></OnlyStartButton>
889
- {/* <MlxVlmServerPanel /> */}
1100
+ {/* <OnlyStartButton></OnlyStartButton> */}
1101
+ <MlxVlmServerPanel />
890
1102
  <div className="flex flex-col items-center justify-center gap-2 p-8 border border-dashed border-ink-200 rounded-2xl">
891
1103
  <span className="text-ink-500">{ClapperIcon}</span>
892
1104
  <p className="text-xs text-ink-500 italic">