@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.
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
- package/dist/backend/movie-backend/agent/agent-backend.js +169 -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.js +107 -44
- package/dist/backend/movie-backend/render-media.js +28 -254
- package/frontend/index.html +6 -1
- package/frontend/public/lambobo.png +0 -0
- package/frontend/src/movie-app/MediaStudio.tsx +6 -2
- package/frontend/src/movie-app/SetupPage.tsx +98 -63
- package/frontend/src/movie-app/components/Aurora.tsx +13 -0
- package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +5 -8
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +417 -205
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +350 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -67
- package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
- package/frontend/src/movie-app/index.css +275 -8
- package/frontend/src/movie-app/index.html +1 -1
- package/frontend/src/movie-app/stores/aiModelStore.ts +172 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2 -3
- package/frontend/src/movie-app/stores/movieStudioStore.ts +72 -3
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
- 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
|
|
278
|
-
<div className="flex items-center gap-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
<
|
|
408
|
-
Scenes
|
|
409
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
<
|
|
459
|
-
{s.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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-
|
|
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
|
-
|
|
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">
|