@effectnode/media 0.5.0 → 0.7.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/LICENSE +21 -0
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +1 -1
- package/dist/backend/movie-backend/agent/agent-backend.js +172 -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.d.ts +1 -1
- package/dist/backend/movie-backend/generation-queue.js +128 -9
- package/dist/backend/movie-backend/render-media.js +49 -226
- package/frontend/index.html +3 -5
- package/frontend/src/movie-app/SetupPage.tsx +1 -1
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +127 -23
- package/frontend/src/movie-app/components/EditorTabs/SetupAiModelTab.tsx +38 -37
- package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +37 -9
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +0 -33
- package/frontend/src/movie-app/components/ProjectManager.tsx +1 -1
- package/frontend/src/movie-app/index.css +46 -20
- package/frontend/src/movie-app/index.html +1 -1
- package/frontend/src/movie-app/stores/aiModelStore.ts +20 -7
- package/frontend/src/movie-app/stores/generationStore.ts +0 -1
- package/frontend/src/movie-app/stores/movieStudioStore.ts +359 -32
- package/frontend/src/movie-app/stores/queueStore.ts +66 -1
- package/package.json +2 -2
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +0 -664
|
@@ -324,20 +324,38 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
324
324
|
|
|
325
325
|
{/* ===== Submit ===== */}
|
|
326
326
|
{store.generating ? (
|
|
327
|
-
<div className="flex
|
|
328
|
-
<div className="flex items-center gap-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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>
|
|
333
342
|
</div>
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
)}
|
|
341
359
|
</div>
|
|
342
360
|
) : (
|
|
343
361
|
<button
|
|
@@ -387,7 +405,10 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
387
405
|
`character:${c.slug}`,
|
|
388
406
|
);
|
|
389
407
|
return (
|
|
390
|
-
<tr
|
|
408
|
+
<tr
|
|
409
|
+
key={c.slug}
|
|
410
|
+
className="border-b border-ink-200"
|
|
411
|
+
>
|
|
391
412
|
<td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
|
|
392
413
|
{c.slug}
|
|
393
414
|
</td>
|
|
@@ -418,7 +439,9 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
418
439
|
/>
|
|
419
440
|
</div>
|
|
420
441
|
) : (
|
|
421
|
-
<span className="text-ink-300 text-xs"
|
|
442
|
+
<span className="text-ink-300 text-xs">
|
|
443
|
+
—
|
|
444
|
+
</span>
|
|
422
445
|
)}
|
|
423
446
|
</td>
|
|
424
447
|
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
@@ -468,7 +491,10 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
468
491
|
`place:${p.slug}`,
|
|
469
492
|
);
|
|
470
493
|
return (
|
|
471
|
-
<tr
|
|
494
|
+
<tr
|
|
495
|
+
key={p.slug}
|
|
496
|
+
className="border-b border-ink-200"
|
|
497
|
+
>
|
|
472
498
|
<td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
|
|
473
499
|
{p.slug}
|
|
474
500
|
</td>
|
|
@@ -499,14 +525,18 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
499
525
|
/>
|
|
500
526
|
</div>
|
|
501
527
|
) : (
|
|
502
|
-
<span className="text-ink-300 text-xs"
|
|
528
|
+
<span className="text-ink-300 text-xs">
|
|
529
|
+
—
|
|
530
|
+
</span>
|
|
503
531
|
)}
|
|
504
532
|
</td>
|
|
505
533
|
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
506
534
|
<EditableTextarea
|
|
507
535
|
value={p.imagePrompt}
|
|
508
536
|
onChange={(v) =>
|
|
509
|
-
store.updatePlace(p.slug, {
|
|
537
|
+
store.updatePlace(p.slug, {
|
|
538
|
+
imagePrompt: v,
|
|
539
|
+
})
|
|
510
540
|
}
|
|
511
541
|
rows={3}
|
|
512
542
|
/>
|
|
@@ -522,9 +552,47 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
522
552
|
|
|
523
553
|
{/* Scenes */}
|
|
524
554
|
<div>
|
|
525
|
-
<
|
|
526
|
-
Scenes
|
|
527
|
-
|
|
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
|
+
)}
|
|
528
596
|
{store.result.scenes.length === 0 ? (
|
|
529
597
|
<p className="text-xs text-ink-500 italic py-3 border border-dashed border-ink-200 rounded-2xl text-center">
|
|
530
598
|
No scenes found.
|
|
@@ -552,6 +620,7 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
552
620
|
},
|
|
553
621
|
{ key: "place", label: "Place", className: "w-24" },
|
|
554
622
|
{ key: "image", label: "Image", className: "w-16" },
|
|
623
|
+
{ key: "video", label: "Video", className: "w-40" },
|
|
555
624
|
{ key: "script", label: "Script", className: "w-60" },
|
|
556
625
|
{
|
|
557
626
|
key: "voiceover",
|
|
@@ -566,15 +635,26 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
566
635
|
const url = imageUrlFor("scene", s.slug);
|
|
567
636
|
const spinning =
|
|
568
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);
|
|
569
644
|
return (
|
|
570
|
-
<tr
|
|
645
|
+
<tr
|
|
646
|
+
key={s.slug}
|
|
647
|
+
className="border-b border-ink-200"
|
|
648
|
+
>
|
|
571
649
|
<td className="border border-ink-200 px-2 py-1.5 align-top font-mono text-[11px] text-tiffany-700">
|
|
572
650
|
{s.slug}
|
|
573
651
|
</td>
|
|
574
652
|
<td className="border border-ink-200 px-2 py-1.5 align-top">
|
|
575
653
|
<EditableInput
|
|
576
654
|
type="number"
|
|
577
|
-
value={
|
|
655
|
+
value={
|
|
656
|
+
s.duration > 0 ? String(s.duration) : ""
|
|
657
|
+
}
|
|
578
658
|
onChange={(v) =>
|
|
579
659
|
store.updateScene(s.slug, {
|
|
580
660
|
duration: Number(v) || 0,
|
|
@@ -634,6 +714,30 @@ export default function MovieStudioTab({ projectId }: Props) {
|
|
|
634
714
|
}
|
|
635
715
|
/>
|
|
636
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>
|
|
637
741
|
) : (
|
|
638
742
|
<span className="text-ink-300 text-xs">—</span>
|
|
639
743
|
)}
|
|
@@ -191,9 +191,19 @@ const DOWNLOAD_MODELS: {
|
|
|
191
191
|
desc: "Character, place & scene images",
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
|
-
id: "
|
|
195
|
-
name: "
|
|
196
|
-
desc: "
|
|
194
|
+
id: "ltx",
|
|
195
|
+
name: "dgrauet/ltx-2.3-mlx-q8",
|
|
196
|
+
desc: "Video generation",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "tts",
|
|
200
|
+
name: "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
|
|
201
|
+
desc: "Voice-over & speech",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "gemma",
|
|
205
|
+
name: "mlx-community/gemma-4-e4b-it-8bit",
|
|
206
|
+
desc: "Story planning & dialogue (LLM)",
|
|
197
207
|
},
|
|
198
208
|
];
|
|
199
209
|
|
|
@@ -228,7 +238,10 @@ export default function SetupAiModelTab() {
|
|
|
228
238
|
<section>
|
|
229
239
|
<h3 className="mb-2 text-sm font-semibold text-ink-900">Engines</h3>
|
|
230
240
|
<div className="space-y-2">
|
|
231
|
-
<ModelRow
|
|
241
|
+
<ModelRow
|
|
242
|
+
name="mlx-gen"
|
|
243
|
+
desc="Image generation engine (z-image · flux)"
|
|
244
|
+
>
|
|
232
245
|
<StatusBadge
|
|
233
246
|
value={store.mlxgenInstalled}
|
|
234
247
|
okText="Installed"
|
|
@@ -259,6 +272,22 @@ export default function SetupAiModelTab() {
|
|
|
259
272
|
{store.downloading === "mlx-vlm" ? "Installing…" : "Install"}
|
|
260
273
|
</button>
|
|
261
274
|
</ModelRow>
|
|
275
|
+
|
|
276
|
+
<ModelRow name="huggingface-cli" desc="HF CLI for downloading models">
|
|
277
|
+
<StatusBadge
|
|
278
|
+
value={store.hfInstalled}
|
|
279
|
+
okText="Installed"
|
|
280
|
+
missingText="Not installed"
|
|
281
|
+
/>
|
|
282
|
+
<button
|
|
283
|
+
onClick={() => store.installTool("hf-cli")}
|
|
284
|
+
disabled={busy}
|
|
285
|
+
className="flex items-center gap-1.5 whitespace-nowrap rounded-xl border border-tiffany-500 px-3 py-1.5 text-xs font-medium text-tiffany-700 transition-colors hover:bg-tiffany-50 disabled:opacity-50"
|
|
286
|
+
>
|
|
287
|
+
{store.downloading === "hf-cli" ? SpinnerIcon : InstallIcon}
|
|
288
|
+
{store.downloading === "hf-cli" ? "Installing…" : "Install"}
|
|
289
|
+
</button>
|
|
290
|
+
</ModelRow>
|
|
262
291
|
</div>
|
|
263
292
|
</section>
|
|
264
293
|
|
|
@@ -266,27 +295,17 @@ export default function SetupAiModelTab() {
|
|
|
266
295
|
<section>
|
|
267
296
|
<h3 className="mb-2 text-sm font-semibold text-ink-900">Models</h3>
|
|
268
297
|
<div className="space-y-2">
|
|
269
|
-
<ModelRow
|
|
270
|
-
name="mlx-community/gemma-4-e2b-it-4bit"
|
|
271
|
-
desc="Story planning & dialogue (LLM)"
|
|
272
|
-
>
|
|
273
|
-
<StatusBadge
|
|
274
|
-
value={store.mlxVlmInstalled}
|
|
275
|
-
okText="Ready"
|
|
276
|
-
missingText="Not set up"
|
|
277
|
-
/>
|
|
278
|
-
<span className="text-xs italic text-ink-400">
|
|
279
|
-
Downloads on first server start
|
|
280
|
-
</span>
|
|
281
|
-
</ModelRow>
|
|
282
|
-
|
|
283
298
|
{DOWNLOAD_MODELS.map((m) => {
|
|
284
299
|
const downloaded =
|
|
285
300
|
m.id === "z-image"
|
|
286
301
|
? store.zImageDownloaded
|
|
287
302
|
: m.id === "flux"
|
|
288
303
|
? store.fluxDownloaded
|
|
289
|
-
:
|
|
304
|
+
: m.id === "ltx"
|
|
305
|
+
? store.ltxDownloaded
|
|
306
|
+
: m.id === "gemma"
|
|
307
|
+
? store.gemmaDownloaded
|
|
308
|
+
: store.ttsDownloaded;
|
|
290
309
|
return (
|
|
291
310
|
<ModelRow key={m.id} name={m.name} desc={m.desc}>
|
|
292
311
|
<StatusBadge value={downloaded} />
|
|
@@ -301,24 +320,6 @@ export default function SetupAiModelTab() {
|
|
|
301
320
|
</ModelRow>
|
|
302
321
|
);
|
|
303
322
|
})}
|
|
304
|
-
|
|
305
|
-
<ModelRow
|
|
306
|
-
name="dgrauet/ltx-2.3-mlx-q8"
|
|
307
|
-
desc="Video generation"
|
|
308
|
-
>
|
|
309
|
-
<span className="text-xs italic text-ink-400">
|
|
310
|
-
Downloads on first render
|
|
311
|
-
</span>
|
|
312
|
-
</ModelRow>
|
|
313
|
-
|
|
314
|
-
<ModelRow
|
|
315
|
-
name="Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
|
316
|
-
desc="Voice-over & speech"
|
|
317
|
-
>
|
|
318
|
-
<span className="text-xs italic text-ink-400">
|
|
319
|
-
Downloads on first use
|
|
320
|
-
</span>
|
|
321
|
-
</ModelRow>
|
|
322
323
|
</div>
|
|
323
324
|
</section>
|
|
324
325
|
|
|
@@ -126,9 +126,11 @@ function StatusBadge({ status }: { status: QueueTaskStatus }) {
|
|
|
126
126
|
|
|
127
127
|
function TaskRow({
|
|
128
128
|
task,
|
|
129
|
+
showProject = false,
|
|
129
130
|
onCancel,
|
|
130
131
|
}: {
|
|
131
132
|
task: QueueTask;
|
|
133
|
+
showProject?: boolean;
|
|
132
134
|
onCancel: () => void;
|
|
133
135
|
}) {
|
|
134
136
|
const active = task.status === "pending" || task.status === "running";
|
|
@@ -142,6 +144,11 @@ function TaskRow({
|
|
|
142
144
|
<li className="flex flex-col gap-1.5 px-3 py-2 rounded-xl border border-ink-200 bg-white">
|
|
143
145
|
<div className="flex items-center gap-2">
|
|
144
146
|
<StatusBadge status={task.status} />
|
|
147
|
+
{showProject && task.projectId && (
|
|
148
|
+
<span className="px-1.5 py-0.5 rounded bg-ink-100 text-ink-500 text-[10px] font-mono whitespace-nowrap">
|
|
149
|
+
{task.projectId}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
145
152
|
<span className="flex-1 text-sm font-medium text-ink-800 truncate">
|
|
146
153
|
{task.label}
|
|
147
154
|
</span>
|
|
@@ -179,19 +186,25 @@ function TaskRow({
|
|
|
179
186
|
|
|
180
187
|
export default function TaskQueuePanel({ projectId }: Props) {
|
|
181
188
|
const tasks = useQueueStore((s) => s.tasks);
|
|
189
|
+
const allTasks = useQueueStore((s) => s.allTasks);
|
|
190
|
+
const showAll = useQueueStore((s) => s.showAll);
|
|
182
191
|
const paused = useQueueStore((s) => s.paused);
|
|
183
192
|
const cancel = useQueueStore((s) => s.cancel);
|
|
184
193
|
const pause = useQueueStore((s) => s.pause);
|
|
185
194
|
const resume = useQueueStore((s) => s.resume);
|
|
186
195
|
const clearFinished = useQueueStore((s) => s.clearFinished);
|
|
196
|
+
const setShowAll = useQueueStore((s) => s.setShowAll);
|
|
197
|
+
|
|
198
|
+
const displayTasks = showAll ? allTasks : tasks;
|
|
187
199
|
|
|
188
|
-
if (tasks.length === 0 && !paused) return null;
|
|
200
|
+
if (!showAll && tasks.length === 0 && !paused) return null;
|
|
189
201
|
|
|
190
|
-
const hasActive =
|
|
202
|
+
const hasActive = displayTasks.some(
|
|
191
203
|
(t) => t.status === "pending" || t.status === "running",
|
|
192
204
|
);
|
|
193
|
-
const hasFinished =
|
|
194
|
-
(t) =>
|
|
205
|
+
const hasFinished = displayTasks.some(
|
|
206
|
+
(t) =>
|
|
207
|
+
t.status !== "pending" && t.status !== "running" && t.status !== "paused",
|
|
195
208
|
);
|
|
196
209
|
|
|
197
210
|
return (
|
|
@@ -199,9 +212,22 @@ export default function TaskQueuePanel({ projectId }: Props) {
|
|
|
199
212
|
<div className="flex items-center gap-2">
|
|
200
213
|
<span className="text-tiffany-600">{QueueIcon}</span>
|
|
201
214
|
<h3 className="text-sm font-semibold text-ink-900">Generation Queue</h3>
|
|
202
|
-
<span className="text-xs text-ink-500">{
|
|
215
|
+
<span className="text-xs text-ink-500">{displayTasks.length}</span>
|
|
216
|
+
|
|
217
|
+
<button
|
|
218
|
+
onClick={() => setShowAll(!showAll)}
|
|
219
|
+
title={showAll ? "Show only this project" : "Show all projects"}
|
|
220
|
+
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl transition-colors ${
|
|
221
|
+
showAll
|
|
222
|
+
? "bg-tiffany-500 text-ink-950 hover:bg-tiffany-600"
|
|
223
|
+
: "border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900"
|
|
224
|
+
}`}
|
|
225
|
+
>
|
|
226
|
+
{showAll ? "All projects" : "This project"}
|
|
227
|
+
</button>
|
|
228
|
+
|
|
203
229
|
<div className="ml-auto flex items-center gap-2">
|
|
204
|
-
{paused ? (
|
|
230
|
+
{!showAll && paused ? (
|
|
205
231
|
<button
|
|
206
232
|
onClick={() => resume(projectId)}
|
|
207
233
|
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
|
|
@@ -210,6 +236,7 @@ export default function TaskQueuePanel({ projectId }: Props) {
|
|
|
210
236
|
Resume
|
|
211
237
|
</button>
|
|
212
238
|
) : (
|
|
239
|
+
!showAll &&
|
|
213
240
|
hasActive && (
|
|
214
241
|
<button
|
|
215
242
|
onClick={() => pause(projectId)}
|
|
@@ -220,7 +247,7 @@ export default function TaskQueuePanel({ projectId }: Props) {
|
|
|
220
247
|
</button>
|
|
221
248
|
)
|
|
222
249
|
)}
|
|
223
|
-
{hasFinished && (
|
|
250
|
+
{!showAll && hasFinished && (
|
|
224
251
|
<button
|
|
225
252
|
onClick={() => clearFinished(projectId)}
|
|
226
253
|
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900 transition-colors"
|
|
@@ -233,11 +260,12 @@ export default function TaskQueuePanel({ projectId }: Props) {
|
|
|
233
260
|
</div>
|
|
234
261
|
|
|
235
262
|
<ul className="flex flex-col gap-1.5">
|
|
236
|
-
{
|
|
263
|
+
{displayTasks.map((task) => (
|
|
237
264
|
<TaskRow
|
|
238
265
|
key={task.id}
|
|
239
266
|
task={task}
|
|
240
|
-
|
|
267
|
+
showProject={showAll}
|
|
268
|
+
onCancel={() => cancel(task.projectId ?? projectId, task.id)}
|
|
241
269
|
/>
|
|
242
270
|
))}
|
|
243
271
|
</ul>
|
|
@@ -8,7 +8,6 @@ import MovieStudioTab from "./EditorTabs/MovieStudioTab";
|
|
|
8
8
|
import GenerateVideoTab from "./EditorTabs/GenerateVideoTab";
|
|
9
9
|
import AgentTab from "./EditorTabs/AgentTab";
|
|
10
10
|
import StoryWriterTab from "./EditorTabs/StoryWriterTab";
|
|
11
|
-
import CharactersTab from "./EditorTabs/CharactersTab";
|
|
12
11
|
import ExtractImageTab from "./EditorTabs/ExtractImageTab";
|
|
13
12
|
import SceneVisualTab from "./EditorTabs/SceneVisualTab";
|
|
14
13
|
import TextToImageTab from "./EditorTabs/TextToImageTab";
|
|
@@ -173,22 +172,6 @@ export default function ProjectEditorPage() {
|
|
|
173
172
|
</svg>
|
|
174
173
|
);
|
|
175
174
|
|
|
176
|
-
const CharacterIcon = (
|
|
177
|
-
<svg
|
|
178
|
-
width="18"
|
|
179
|
-
height="18"
|
|
180
|
-
viewBox="0 0 24 24"
|
|
181
|
-
fill="none"
|
|
182
|
-
stroke="currentColor"
|
|
183
|
-
strokeWidth="2"
|
|
184
|
-
strokeLinecap="round"
|
|
185
|
-
strokeLinejoin="round"
|
|
186
|
-
>
|
|
187
|
-
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
|
188
|
-
<circle cx="12" cy="7" r="4" />
|
|
189
|
-
</svg>
|
|
190
|
-
);
|
|
191
|
-
|
|
192
175
|
const ExtractIcon = (
|
|
193
176
|
<svg
|
|
194
177
|
width="18"
|
|
@@ -471,17 +454,6 @@ export default function ProjectEditorPage() {
|
|
|
471
454
|
{StoryWriterIcon}
|
|
472
455
|
Story Writer
|
|
473
456
|
</button>
|
|
474
|
-
<button
|
|
475
|
-
onClick={() => store.setActiveTab("characters")}
|
|
476
|
-
className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
|
|
477
|
-
store.activeTab === "characters"
|
|
478
|
-
? "bg-tiffany-500/10 text-tiffany-700 shadow-glow-sm"
|
|
479
|
-
: "text-ink-600 hover:bg-ink-100 hover:text-ink-800"
|
|
480
|
-
}`}
|
|
481
|
-
>
|
|
482
|
-
{CharacterIcon}
|
|
483
|
-
Characters
|
|
484
|
-
</button>
|
|
485
457
|
<button
|
|
486
458
|
onClick={() => store.setActiveTab("extract")}
|
|
487
459
|
className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium rounded-2xl transition-all ${
|
|
@@ -597,11 +569,6 @@ export default function ProjectEditorPage() {
|
|
|
597
569
|
<StoryWriterTab projectId={id!} />
|
|
598
570
|
)}
|
|
599
571
|
|
|
600
|
-
{/* ========== CHARACTERS PANEL ========== */}
|
|
601
|
-
{store.activeTab === "characters" && (
|
|
602
|
-
<CharactersTab projectId={id!} />
|
|
603
|
-
)}
|
|
604
|
-
|
|
605
572
|
{/* ========== EXTRACT IMAGE PANEL ========== */}
|
|
606
573
|
{store.activeTab === "extract" && <ExtractImageTab projectId={id!} />}
|
|
607
574
|
|
|
@@ -168,7 +168,7 @@ export default function ProjectManager() {
|
|
|
168
168
|
<header className="flex flex-col gap-7 md:flex-row md:items-end md:justify-between">
|
|
169
169
|
<div className="max-w-xl">
|
|
170
170
|
<p className="text-[11px] font-semibold uppercase tracking-[0.24em] text-tiffany-600">
|
|
171
|
-
|
|
171
|
+
EffectNode Media
|
|
172
172
|
</p>
|
|
173
173
|
<h1 className="mt-3 font-display text-4xl font-light leading-[1.06] text-ink-900 md:text-5xl">
|
|
174
174
|
Make the movies{" "}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
3
|
/* ==========================================================================
|
|
4
|
-
|
|
4
|
+
EffectNode Media — design tokens
|
|
5
5
|
A dreamy, tiffany-blue film studio. Gradients read like light through
|
|
6
6
|
shallow water: tiffany teal → aqua → periwinkle → blush.
|
|
7
7
|
========================================================================== */
|
|
8
8
|
|
|
9
9
|
@theme {
|
|
10
10
|
/* ---- Type ---- */
|
|
11
|
-
--font-sans:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
--font-sans:
|
|
12
|
+
"Avenir Next", Avenir, "Helvetica Neue", -apple-system, BlinkMacSystemFont,
|
|
13
|
+
"Segoe UI", Roboto, sans-serif;
|
|
14
|
+
--font-display:
|
|
15
|
+
"Cormorant", "Iowan Old Style", "Baskerville", "Georgia", serif;
|
|
16
|
+
--font-mono:
|
|
17
|
+
ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
|
|
16
18
|
"Liberation Mono", monospace;
|
|
17
19
|
|
|
18
20
|
/* ---- Tiffany (the brand) ---- */
|
|
@@ -48,13 +50,14 @@
|
|
|
48
50
|
--color-dream-mint: #c8f1e6;
|
|
49
51
|
|
|
50
52
|
/* ---- Shadows ---- */
|
|
51
|
-
--shadow-glow:
|
|
53
|
+
--shadow-glow:
|
|
54
|
+
0 8px 30px -6px rgba(10, 186, 181, 0.55),
|
|
52
55
|
0 2px 10px -2px rgba(10, 186, 181, 0.4);
|
|
53
56
|
--shadow-glow-sm: 0 0 14px -2px rgba(10, 186, 181, 0.45);
|
|
54
|
-
--shadow-card:
|
|
55
|
-
0 10px 30px -12px rgba(13, 25, 28, 0.16);
|
|
56
|
-
--shadow-card-hover:
|
|
57
|
-
0 18px 44px -14px rgba(10, 186, 181, 0.35);
|
|
57
|
+
--shadow-card:
|
|
58
|
+
0 1px 2px rgba(13, 25, 28, 0.04), 0 10px 30px -12px rgba(13, 25, 28, 0.16);
|
|
59
|
+
--shadow-card-hover:
|
|
60
|
+
0 2px 4px rgba(13, 25, 28, 0.05), 0 18px 44px -14px rgba(10, 186, 181, 0.35);
|
|
58
61
|
--shadow-modal: 0 24px 80px -16px rgba(13, 25, 28, 0.35);
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -65,16 +68,28 @@
|
|
|
65
68
|
body {
|
|
66
69
|
margin: 0;
|
|
67
70
|
background-color: #f3f7f7;
|
|
68
|
-
background-image:
|
|
71
|
+
background-image:
|
|
72
|
+
radial-gradient(
|
|
69
73
|
120% 90% at 85% -10%,
|
|
70
74
|
rgba(183, 225, 251, 0.5),
|
|
71
75
|
transparent 60%
|
|
72
76
|
),
|
|
73
|
-
radial-gradient(
|
|
77
|
+
radial-gradient(
|
|
78
|
+
100% 80% at 0% 0%,
|
|
79
|
+
rgba(255, 214, 234, 0.4),
|
|
80
|
+
transparent 55%
|
|
81
|
+
);
|
|
74
82
|
background-attachment: fixed;
|
|
75
83
|
color: #283a40;
|
|
76
|
-
font-family:
|
|
77
|
-
|
|
84
|
+
font-family:
|
|
85
|
+
"Avenir Next",
|
|
86
|
+
Avenir,
|
|
87
|
+
"Helvetica Neue",
|
|
88
|
+
-apple-system,
|
|
89
|
+
BlinkMacSystemFont,
|
|
90
|
+
"Segoe UI",
|
|
91
|
+
Roboto,
|
|
92
|
+
sans-serif;
|
|
78
93
|
-webkit-font-smoothing: antialiased;
|
|
79
94
|
-moz-osx-font-smoothing: grayscale;
|
|
80
95
|
}
|
|
@@ -82,7 +97,8 @@ body {
|
|
|
82
97
|
code,
|
|
83
98
|
pre,
|
|
84
99
|
.font-mono {
|
|
85
|
-
font-family:
|
|
100
|
+
font-family:
|
|
101
|
+
ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas,
|
|
86
102
|
"Liberation Mono", monospace;
|
|
87
103
|
}
|
|
88
104
|
|
|
@@ -126,7 +142,8 @@ pre,
|
|
|
126
142
|
z-index: 0;
|
|
127
143
|
overflow: hidden;
|
|
128
144
|
pointer-events: none;
|
|
129
|
-
background:
|
|
145
|
+
background:
|
|
146
|
+
radial-gradient(
|
|
130
147
|
120% 90% at 85% -10%,
|
|
131
148
|
rgba(183, 225, 251, 0.55),
|
|
132
149
|
transparent 60%
|
|
@@ -251,7 +268,12 @@ pre,
|
|
|
251
268
|
}
|
|
252
269
|
|
|
253
270
|
.thumb {
|
|
254
|
-
background-image: linear-gradient(
|
|
271
|
+
background-image: linear-gradient(
|
|
272
|
+
135deg,
|
|
273
|
+
#b0e8e3 0%,
|
|
274
|
+
#81d8d0 50%,
|
|
275
|
+
#c3b4f4 100%
|
|
276
|
+
);
|
|
255
277
|
}
|
|
256
278
|
|
|
257
279
|
.btn-primary {
|
|
@@ -263,12 +285,16 @@ pre,
|
|
|
263
285
|
);
|
|
264
286
|
color: #ffffff;
|
|
265
287
|
box-shadow: var(--shadow-glow);
|
|
266
|
-
transition:
|
|
288
|
+
transition:
|
|
289
|
+
filter 150ms ease,
|
|
290
|
+
box-shadow 150ms ease,
|
|
291
|
+
transform 150ms ease;
|
|
267
292
|
}
|
|
268
293
|
|
|
269
294
|
.btn-primary:hover {
|
|
270
295
|
filter: brightness(1.05);
|
|
271
|
-
box-shadow:
|
|
296
|
+
box-shadow:
|
|
297
|
+
0 10px 34px -6px rgba(10, 186, 181, 0.6),
|
|
272
298
|
0 4px 12px -4px rgba(10, 186, 181, 0.45);
|
|
273
299
|
}
|
|
274
300
|
|