@effectnode/media 0.3.0 → 0.5.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.js +4 -4
- package/dist/backend/movie-backend/agent/prompt/story-writer.txt +137 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/edit-image.js +1 -1
- package/dist/backend/movie-backend/agent/tools/get-time.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/grep-files.js +1 -1
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +1 -1
- package/dist/backend/movie-backend/agent/tools/index.d.ts +2 -2
- package/dist/backend/movie-backend/agent/tools/index.js +13 -13
- package/dist/backend/movie-backend/agent/tools/list-files.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/list-files.js +1 -1
- package/dist/backend/movie-backend/agent/tools/read-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/read-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/remove-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/rename-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/show-image.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/show-image.js +1 -1
- package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/stitch-videos.js +1 -1
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +1 -1
- package/dist/backend/movie-backend/agent/tools/update-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/update-file.js +1 -1
- package/dist/backend/movie-backend/agent/tools/write-file.d.ts +1 -1
- package/dist/backend/movie-backend/agent/tools/write-file.js +1 -1
- package/dist/backend/movie-backend/core.js +4 -4
- package/dist/backend/movie-backend/generation-queue.js +102 -46
- package/dist/backend/movie-backend/render-media.js +1 -1
- package/frontend/index.html +8 -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 +297 -189
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +345 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +40 -34
- package/frontend/src/movie-app/components/ProjectManager.tsx +80 -52
- package/frontend/src/movie-app/index.css +260 -19
- package/frontend/src/movie-app/stores/aiModelStore.ts +163 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2 -2
- package/frontend/src/movie-app/stores/movieStudioStore.ts +45 -0
- package/package.json +2 -2
- 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">
|
|
@@ -331,25 +381,60 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
331
381
|
]}
|
|
332
382
|
/>
|
|
333
383
|
<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
|
-
|
|
384
|
+
{store.result.characters.map((c) => {
|
|
385
|
+
const url = imageUrlFor("character", c.slug);
|
|
386
|
+
const spinning = store.regenerating.includes(
|
|
387
|
+
`character:${c.slug}`,
|
|
388
|
+
);
|
|
389
|
+
return (
|
|
390
|
+
<tr key={c.slug} className="border-b border-ink-200">
|
|
391
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
|
|
392
|
+
{c.slug}
|
|
393
|
+
</td>
|
|
394
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[140px]">
|
|
395
|
+
<EditableInput
|
|
396
|
+
value={c.name}
|
|
397
|
+
onChange={(v) =>
|
|
398
|
+
store.updateCharacter(c.slug, { name: v })
|
|
399
|
+
}
|
|
400
|
+
/>
|
|
401
|
+
</td>
|
|
402
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
403
|
+
{url ? (
|
|
404
|
+
<div className="flex flex-col items-start">
|
|
405
|
+
<div className="h-16 w-16">
|
|
406
|
+
<Thumb url={url} filename={c.slug} />
|
|
407
|
+
</div>
|
|
408
|
+
<RegenerateButton
|
|
409
|
+
spinning={spinning}
|
|
410
|
+
onClick={() =>
|
|
411
|
+
store.regenerateAsset(
|
|
412
|
+
projectId,
|
|
413
|
+
"character",
|
|
414
|
+
c.slug,
|
|
415
|
+
c.imagePrompt,
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
) : (
|
|
421
|
+
<span className="text-ink-300 text-xs">—</span>
|
|
422
|
+
)}
|
|
423
|
+
</td>
|
|
424
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
425
|
+
<EditableTextarea
|
|
426
|
+
value={c.imagePrompt}
|
|
427
|
+
onChange={(v) =>
|
|
428
|
+
store.updateCharacter(c.slug, {
|
|
429
|
+
imagePrompt: v,
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
rows={3}
|
|
433
|
+
/>
|
|
434
|
+
</td>
|
|
435
|
+
</tr>
|
|
436
|
+
);
|
|
437
|
+
})}
|
|
353
438
|
</tbody>
|
|
354
439
|
</table>
|
|
355
440
|
</div>
|
|
@@ -377,25 +462,58 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
377
462
|
]}
|
|
378
463
|
/>
|
|
379
464
|
<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
|
-
|
|
465
|
+
{store.result.places.map((p) => {
|
|
466
|
+
const url = imageUrlFor("place", p.slug);
|
|
467
|
+
const spinning = store.regenerating.includes(
|
|
468
|
+
`place:${p.slug}`,
|
|
469
|
+
);
|
|
470
|
+
return (
|
|
471
|
+
<tr key={p.slug} className="border-b border-ink-200">
|
|
472
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
|
|
473
|
+
{p.slug}
|
|
474
|
+
</td>
|
|
475
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[140px]">
|
|
476
|
+
<EditableInput
|
|
477
|
+
value={p.name}
|
|
478
|
+
onChange={(v) =>
|
|
479
|
+
store.updatePlace(p.slug, { name: v })
|
|
480
|
+
}
|
|
481
|
+
/>
|
|
482
|
+
</td>
|
|
483
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
484
|
+
{url ? (
|
|
485
|
+
<div className="flex flex-col items-start">
|
|
486
|
+
<div className="h-16 w-16">
|
|
487
|
+
<Thumb url={url} filename={p.slug} />
|
|
488
|
+
</div>
|
|
489
|
+
<RegenerateButton
|
|
490
|
+
spinning={spinning}
|
|
491
|
+
onClick={() =>
|
|
492
|
+
store.regenerateAsset(
|
|
493
|
+
projectId,
|
|
494
|
+
"place",
|
|
495
|
+
p.slug,
|
|
496
|
+
p.imagePrompt,
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
/>
|
|
500
|
+
</div>
|
|
501
|
+
) : (
|
|
502
|
+
<span className="text-ink-300 text-xs">—</span>
|
|
503
|
+
)}
|
|
504
|
+
</td>
|
|
505
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
506
|
+
<EditableTextarea
|
|
507
|
+
value={p.imagePrompt}
|
|
508
|
+
onChange={(v) =>
|
|
509
|
+
store.updatePlace(p.slug, { imagePrompt: v })
|
|
510
|
+
}
|
|
511
|
+
rows={3}
|
|
512
|
+
/>
|
|
513
|
+
</td>
|
|
514
|
+
</tr>
|
|
515
|
+
);
|
|
516
|
+
})}
|
|
399
517
|
</tbody>
|
|
400
518
|
</table>
|
|
401
519
|
</div>
|
|
@@ -444,57 +562,137 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
444
562
|
]}
|
|
445
563
|
/>
|
|
446
564
|
<tbody>
|
|
447
|
-
{store.result.scenes.map((s) =>
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
<
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
565
|
+
{store.result.scenes.map((s) => {
|
|
566
|
+
const url = imageUrlFor("scene", s.slug);
|
|
567
|
+
const spinning =
|
|
568
|
+
store.regeneratingSceneImages.includes(s.slug);
|
|
569
|
+
return (
|
|
570
|
+
<tr key={s.slug} className="border-b border-ink-200">
|
|
571
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
|
|
572
|
+
{s.slug}
|
|
573
|
+
</td>
|
|
574
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
575
|
+
<EditableInput
|
|
576
|
+
type="number"
|
|
577
|
+
value={s.duration > 0 ? String(s.duration) : ""}
|
|
578
|
+
onChange={(v) =>
|
|
579
|
+
store.updateScene(s.slug, {
|
|
580
|
+
duration: Number(v) || 0,
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
className="w-20"
|
|
584
|
+
/>
|
|
585
|
+
</td>
|
|
586
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[150px]">
|
|
587
|
+
<EditableTextarea
|
|
588
|
+
value={s.description}
|
|
589
|
+
onChange={(v) =>
|
|
590
|
+
store.updateScene(s.slug, {
|
|
591
|
+
description: v,
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
rows={3}
|
|
595
|
+
/>
|
|
596
|
+
</td>
|
|
597
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[150px]">
|
|
598
|
+
<EditableTextarea
|
|
599
|
+
mono
|
|
600
|
+
value={s.characterSlugs.join(", ")}
|
|
601
|
+
onChange={(v) =>
|
|
602
|
+
store.updateScene(s.slug, {
|
|
603
|
+
characterSlugs: v
|
|
604
|
+
.split(",")
|
|
605
|
+
.map((x) => x.trim())
|
|
606
|
+
.filter(Boolean),
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
rows={2}
|
|
610
|
+
/>
|
|
611
|
+
</td>
|
|
612
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[150px]">
|
|
613
|
+
<EditableInput
|
|
614
|
+
value={s.placeSlug}
|
|
615
|
+
onChange={(v) =>
|
|
616
|
+
store.updateScene(s.slug, { placeSlug: v })
|
|
617
|
+
}
|
|
618
|
+
className="font-mono text-[11px]"
|
|
619
|
+
/>
|
|
620
|
+
</td>
|
|
621
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[200px]">
|
|
622
|
+
{url ? (
|
|
623
|
+
<div className="flex flex-col items-start">
|
|
624
|
+
<div className="h-16 w-16">
|
|
625
|
+
<Thumb url={url} filename={s.slug} />
|
|
481
626
|
</div>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
627
|
+
<RegenerateButton
|
|
628
|
+
spinning={spinning}
|
|
629
|
+
onClick={() =>
|
|
630
|
+
store.regenerateSceneImage(
|
|
631
|
+
projectId,
|
|
632
|
+
s.slug,
|
|
633
|
+
)
|
|
634
|
+
}
|
|
635
|
+
/>
|
|
636
|
+
</div>
|
|
637
|
+
) : (
|
|
638
|
+
<span className="text-ink-300 text-xs">—</span>
|
|
639
|
+
)}
|
|
640
|
+
</td>
|
|
641
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
|
|
642
|
+
<EditableTextarea
|
|
643
|
+
mono
|
|
644
|
+
value={s.scriptLines
|
|
645
|
+
.map((l) => `${l.characterSlug}: ${l.line}`)
|
|
646
|
+
.join("\n")}
|
|
647
|
+
onChange={(v) =>
|
|
648
|
+
store.updateScene(s.slug, {
|
|
649
|
+
scriptLines: v
|
|
650
|
+
.split("\n")
|
|
651
|
+
.map((ln) => {
|
|
652
|
+
const i = ln.indexOf(":");
|
|
653
|
+
return i === -1
|
|
654
|
+
? {
|
|
655
|
+
characterSlug: "",
|
|
656
|
+
line: ln.trim(),
|
|
657
|
+
}
|
|
658
|
+
: {
|
|
659
|
+
characterSlug: ln
|
|
660
|
+
.slice(0, i)
|
|
661
|
+
.trim(),
|
|
662
|
+
line: ln.slice(i + 1).trim(),
|
|
663
|
+
};
|
|
664
|
+
})
|
|
665
|
+
.filter((l) =>
|
|
666
|
+
Boolean(l.characterSlug || l.line),
|
|
667
|
+
),
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
rows={4}
|
|
671
|
+
/>
|
|
672
|
+
</td>
|
|
673
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
|
|
674
|
+
<EditableTextarea
|
|
675
|
+
value={s.voiceOver}
|
|
676
|
+
onChange={(v) =>
|
|
677
|
+
store.updateScene(s.slug, { voiceOver: v })
|
|
678
|
+
}
|
|
679
|
+
rows={3}
|
|
680
|
+
/>
|
|
681
|
+
</td>
|
|
682
|
+
<td className="border border-ink-200 px-2 py-1.5 align-top min-w-[300px]">
|
|
683
|
+
<EditableTextarea
|
|
684
|
+
value={s.imagePrompt}
|
|
685
|
+
onChange={(v) =>
|
|
686
|
+
store.updateScene(s.slug, {
|
|
687
|
+
imagePrompt: v,
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
rows={3}
|
|
691
|
+
/>
|
|
692
|
+
</td>
|
|
693
|
+
</tr>
|
|
694
|
+
);
|
|
695
|
+
})}
|
|
498
696
|
</tbody>
|
|
499
697
|
</table>
|
|
500
698
|
</div>
|
|
@@ -665,7 +863,7 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
665
863
|
onClick={() =>
|
|
666
864
|
openPreview(fullUrl, img.slug, "image")
|
|
667
865
|
}
|
|
668
|
-
className="aspect-
|
|
866
|
+
className="aspect-square object-cover object-center w-full cursor-zoom-in"
|
|
669
867
|
/>
|
|
670
868
|
</div>
|
|
671
869
|
<div className="flex items-center justify-between gap-1">
|
|
@@ -691,96 +889,6 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
691
889
|
</div>
|
|
692
890
|
)}
|
|
693
891
|
|
|
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
892
|
{/* ===== Render ===== */}
|
|
785
893
|
{store.result && (
|
|
786
894
|
<div className="flex flex-col gap-3">
|
|
@@ -885,8 +993,8 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
885
993
|
</>
|
|
886
994
|
) : (
|
|
887
995
|
<>
|
|
888
|
-
<OnlyStartButton></OnlyStartButton>
|
|
889
|
-
|
|
996
|
+
{/* <OnlyStartButton></OnlyStartButton> */}
|
|
997
|
+
<MlxVlmServerPanel />
|
|
890
998
|
<div className="flex flex-col items-center justify-center gap-2 p-8 border border-dashed border-ink-200 rounded-2xl">
|
|
891
999
|
<span className="text-ink-500">{ClapperIcon}</span>
|
|
892
1000
|
<p className="text-xs text-ink-500 italic">
|