@effectnode/media 0.1.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/bin/effectnode-media +2 -0
- package/dist/backend/index.d.ts +8 -0
- package/dist/backend/index.js +13 -0
- package/dist/backend/movie-backend/agent/agent-backend.d.ts +16 -0
- package/dist/backend/movie-backend/agent/agent-backend.js +703 -0
- package/dist/backend/movie-backend/agent/storyprompt.d.ts +1 -0
- package/dist/backend/movie-backend/agent/storyprompt.js +90 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/edit-image.js +192 -0
- package/dist/backend/movie-backend/agent/tools/get-time.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/get-time.js +7 -0
- package/dist/backend/movie-backend/agent/tools/grep-files.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/grep-files.js +59 -0
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/image-to-video-generation.js +160 -0
- package/dist/backend/movie-backend/agent/tools/index.d.ts +7 -0
- package/dist/backend/movie-backend/agent/tools/index.js +59 -0
- package/dist/backend/movie-backend/agent/tools/list-files.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/list-files.js +17 -0
- package/dist/backend/movie-backend/agent/tools/read-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/read-file.js +28 -0
- package/dist/backend/movie-backend/agent/tools/remove-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/remove-file.js +22 -0
- package/dist/backend/movie-backend/agent/tools/rename-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/rename-file.js +37 -0
- package/dist/backend/movie-backend/agent/tools/show-image.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/show-image.js +28 -0
- package/dist/backend/movie-backend/agent/tools/stitch-videos.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/stitch-videos.js +96 -0
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/text-to-video-generation.js +125 -0
- package/dist/backend/movie-backend/agent/tools/types.d.ts +15 -0
- package/dist/backend/movie-backend/agent/tools/types.js +1 -0
- package/dist/backend/movie-backend/agent/tools/update-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/update-file.js +27 -0
- package/dist/backend/movie-backend/agent/tools/write-file.d.ts +3 -0
- package/dist/backend/movie-backend/agent/tools/write-file.js +26 -0
- package/dist/backend/movie-backend/agent/workspace.d.ts +25 -0
- package/dist/backend/movie-backend/agent/workspace.js +124 -0
- package/dist/backend/movie-backend/core.d.ts +4 -0
- package/dist/backend/movie-backend/core.js +1167 -0
- package/dist/backend/movie-backend/generation-queue.d.ts +24 -0
- package/dist/backend/movie-backend/generation-queue.js +766 -0
- package/dist/backend/movie-backend/process.d.ts +21 -0
- package/dist/backend/movie-backend/process.js +98 -0
- package/dist/backend/movie-backend/render-media.d.ts +48 -0
- package/dist/backend/movie-backend/render-media.js +3311 -0
- package/dist/backend/routes/health.d.ts +1 -0
- package/dist/backend/routes/health.js +9 -0
- package/dist/backend/routes/index.d.ts +1 -0
- package/dist/backend/routes/index.js +11 -0
- package/dist/backend/routes/media.d.ts +1 -0
- package/dist/backend/routes/media.js +13 -0
- package/dist/backend/routes/projects.d.ts +8 -0
- package/dist/backend/routes/projects.js +68 -0
- package/dist/backend/routes/voice.d.ts +1 -0
- package/dist/backend/routes/voice.js +39 -0
- package/dist/backend/routes.d.ts +1 -0
- package/dist/backend/routes.js +20 -0
- package/dist/backend/server.d.ts +1 -0
- package/dist/backend/server.js +23 -0
- package/dist/backend/workspace.d.ts +5 -0
- package/dist/backend/workspace.js +27 -0
- package/dist/backend/ws/index.d.ts +3 -0
- package/dist/backend/ws/index.js +27 -0
- package/dist/backend/ws/messages.d.ts +8 -0
- package/dist/backend/ws/messages.js +21 -0
- package/dist/backend/ws.d.ts +3 -0
- package/dist/backend/ws.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -0
- package/frontend/index.html +22 -0
- package/frontend/public/ffmpeg/ffmpeg-core.js +16 -0
- package/frontend/public/ffmpeg/ffmpeg-core.wasm +0 -0
- package/frontend/src/index.css +46 -0
- package/frontend/src/main.tsx +13 -0
- package/frontend/src/movie-app/AppRouter.tsx +14 -0
- package/frontend/src/movie-app/MediaStudio.tsx +9 -0
- package/frontend/src/movie-app/SetupPage.tsx +293 -0
- package/frontend/src/movie-app/components/AgentWorkspace.tsx +574 -0
- package/frontend/src/movie-app/components/ChatUI/ChatUI.tsx +655 -0
- package/frontend/src/movie-app/components/ChatUI/MarkdownMessage.tsx +267 -0
- package/frontend/src/movie-app/components/EditorTabs/AgentTab.tsx +61 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchImageToVideoTab.tsx +927 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchVideoTab.tsx +777 -0
- package/frontend/src/movie-app/components/EditorTabs/BatchVoiceVideoTab.tsx +913 -0
- package/frontend/src/movie-app/components/EditorTabs/CharacterSheet.tsx +234 -0
- package/frontend/src/movie-app/components/EditorTabs/CharactersTab.tsx +664 -0
- package/frontend/src/movie-app/components/EditorTabs/CropTool.tsx +251 -0
- package/frontend/src/movie-app/components/EditorTabs/ExtendVideoTab.tsx +305 -0
- package/frontend/src/movie-app/components/EditorTabs/ExtractImageTab.tsx +249 -0
- package/frontend/src/movie-app/components/EditorTabs/FastImageEditTab.tsx +555 -0
- package/frontend/src/movie-app/components/EditorTabs/GenerateVideoTab.tsx +722 -0
- package/frontend/src/movie-app/components/EditorTabs/LlmServerTab.tsx +40 -0
- package/frontend/src/movie-app/components/EditorTabs/MlxVlmServerPanel.tsx +490 -0
- package/frontend/src/movie-app/components/EditorTabs/MovieStudioTab.tsx +938 -0
- package/frontend/src/movie-app/components/EditorTabs/ReferencesToVideoTab.tsx +881 -0
- package/frontend/src/movie-app/components/EditorTabs/SceneVisualTab.tsx +267 -0
- package/frontend/src/movie-app/components/EditorTabs/StoryWriterTab.tsx +51 -0
- package/frontend/src/movie-app/components/EditorTabs/TaskQueuePanel.tsx +246 -0
- package/frontend/src/movie-app/components/EditorTabs/TerminalLogPanel.tsx +49 -0
- package/frontend/src/movie-app/components/EditorTabs/TextToImageTab.tsx +574 -0
- package/frontend/src/movie-app/components/EditorTabs/VoiceChatPanel.tsx +320 -0
- package/frontend/src/movie-app/components/ProjectEditorPage.tsx +636 -0
- package/frontend/src/movie-app/components/ProjectManager.tsx +381 -0
- package/frontend/src/movie-app/index.css +46 -0
- package/frontend/src/movie-app/index.html +16 -0
- package/frontend/src/movie-app/lambobo.png +0 -0
- package/frontend/src/movie-app/lib/batchImageToVideoStorage.ts +69 -0
- package/frontend/src/movie-app/lib/batchVideoStorage.ts +68 -0
- package/frontend/src/movie-app/lib/batchVoiceStorage.ts +75 -0
- package/frontend/src/movie-app/lib/ffmpeg.ts +42 -0
- package/frontend/src/movie-app/lib/referencesToVideoStorage.ts +64 -0
- package/frontend/src/movie-app/lib/textToImageStorage.ts +61 -0
- package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.js +16 -0
- package/frontend/src/movie-app/public/ffmpeg/ffmpeg-core.wasm +0 -0
- package/frontend/src/movie-app/stores/batchImageToVideoStore.ts +736 -0
- package/frontend/src/movie-app/stores/batchVideoStore.ts +670 -0
- package/frontend/src/movie-app/stores/batchVoiceStore.ts +990 -0
- package/frontend/src/movie-app/stores/characterStore.ts +109 -0
- package/frontend/src/movie-app/stores/chatStore.ts +459 -0
- package/frontend/src/movie-app/stores/generationStore.ts +2140 -0
- package/frontend/src/movie-app/stores/logStore.ts +25 -0
- package/frontend/src/movie-app/stores/movieStudioStore.ts +709 -0
- package/frontend/src/movie-app/stores/projectStore.ts +124 -0
- package/frontend/src/movie-app/stores/queueStore.ts +214 -0
- package/frontend/src/movie-app/stores/referencesToVideoStore.ts +467 -0
- package/frontend/src/movie-app/stores/sceneVisualStore.ts +251 -0
- package/frontend/src/movie-app/stores/voiceChatStore.ts +170 -0
- package/frontend/src/movie-app/stores/workspaceStore.ts +158 -0
- package/frontend/src/vite-env.d.ts +1 -0
- package/frontend/tsconfig.json +19 -0
- package/package.json +65 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface CropRect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
w: number;
|
|
7
|
+
h: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
image: string;
|
|
12
|
+
onApply: (dataUrl: string) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MAX_W = 420;
|
|
17
|
+
const MAX_H = 320;
|
|
18
|
+
const MIN_SIZE = 20;
|
|
19
|
+
|
|
20
|
+
const ASPECTS: { label: string; value: number | null }[] = [
|
|
21
|
+
{ label: "Free", value: null },
|
|
22
|
+
{ label: "1:1", value: 1 },
|
|
23
|
+
{ label: "4:3", value: 4 / 3 },
|
|
24
|
+
{ label: "3:4", value: 3 / 4 },
|
|
25
|
+
{ label: "16:9", value: 16 / 9 },
|
|
26
|
+
{ label: "9:16", value: 9 / 16 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function fitDisplay(nw: number, nh: number): { w: number; h: number } {
|
|
30
|
+
const scale = Math.min(MAX_W / nw, MAX_H / nh, 1);
|
|
31
|
+
return {
|
|
32
|
+
w: Math.max(1, Math.round(nw * scale)),
|
|
33
|
+
h: Math.max(1, Math.round(nh * scale)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function initialCrop(w: number, h: number, aspect: number | null): CropRect {
|
|
38
|
+
if (!aspect) return { x: 0, y: 0, w, h };
|
|
39
|
+
let cw = w;
|
|
40
|
+
let ch = cw / aspect;
|
|
41
|
+
if (ch > h) {
|
|
42
|
+
ch = h;
|
|
43
|
+
cw = ch * aspect;
|
|
44
|
+
}
|
|
45
|
+
return { x: (w - cw) / 2, y: (h - ch) / 2, w: cw, h: ch };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clamp(v: number, min: number, max: number): number {
|
|
49
|
+
return Math.max(min, Math.min(max, v));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function CropTool({ image, onApply, onCancel }: Props) {
|
|
53
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const dragRef = useRef<{
|
|
55
|
+
mode: "move" | "resize";
|
|
56
|
+
startX: number;
|
|
57
|
+
startY: number;
|
|
58
|
+
orig: CropRect;
|
|
59
|
+
} | null>(null);
|
|
60
|
+
|
|
61
|
+
const [natural, setNatural] = useState<{ w: number; h: number } | null>(null);
|
|
62
|
+
const [display, setDisplay] = useState<{ w: number; h: number } | null>(null);
|
|
63
|
+
const [aspect, setAspect] = useState<number | null>(null);
|
|
64
|
+
const [crop, setCrop] = useState<CropRect | null>(null);
|
|
65
|
+
|
|
66
|
+
const handleLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
|
67
|
+
const img = e.currentTarget;
|
|
68
|
+
const nw = img.naturalWidth;
|
|
69
|
+
const nh = img.naturalHeight;
|
|
70
|
+
if (!nw || !nh) return;
|
|
71
|
+
setNatural({ w: nw, h: nh });
|
|
72
|
+
const d = fitDisplay(nw, nh);
|
|
73
|
+
setDisplay(d);
|
|
74
|
+
setCrop(initialCrop(d.w, d.h, aspect));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const changeAspect = (a: number | null) => {
|
|
78
|
+
setAspect(a);
|
|
79
|
+
if (display) setCrop(initialCrop(display.w, display.h, a));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const startDrag = (mode: "move" | "resize") => (e: React.MouseEvent) => {
|
|
83
|
+
if (!crop || !display) return;
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
dragRef.current = { mode, startX: e.clientX, startY: e.clientY, orig: crop };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const onMove = (e: MouseEvent) => {
|
|
91
|
+
const drag = dragRef.current;
|
|
92
|
+
if (!drag || !display) return;
|
|
93
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
94
|
+
if (!rect) return;
|
|
95
|
+
const dx = e.clientX - drag.startX;
|
|
96
|
+
const dy = e.clientY - drag.startY;
|
|
97
|
+
|
|
98
|
+
if (drag.mode === "move") {
|
|
99
|
+
setCrop({
|
|
100
|
+
...drag.orig,
|
|
101
|
+
x: clamp(drag.orig.x + dx, 0, display.w - drag.orig.w),
|
|
102
|
+
y: clamp(drag.orig.y + dy, 0, display.h - drag.orig.h),
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// resize (bottom-right handle, anchored at top-left)
|
|
108
|
+
const maxW = display.w - drag.orig.x;
|
|
109
|
+
const maxH = display.h - drag.orig.y;
|
|
110
|
+
let w = clamp(drag.orig.w + dx, MIN_SIZE, maxW);
|
|
111
|
+
let h = aspect
|
|
112
|
+
? w / aspect
|
|
113
|
+
: clamp(drag.orig.h + dy, MIN_SIZE, maxH);
|
|
114
|
+
if (aspect && h > maxH) {
|
|
115
|
+
h = maxH;
|
|
116
|
+
w = h * aspect;
|
|
117
|
+
}
|
|
118
|
+
setCrop({ ...drag.orig, w, h });
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const onUp = () => {
|
|
122
|
+
dragRef.current = null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
window.addEventListener("mousemove", onMove);
|
|
126
|
+
window.addEventListener("mouseup", onUp);
|
|
127
|
+
return () => {
|
|
128
|
+
window.removeEventListener("mousemove", onMove);
|
|
129
|
+
window.removeEventListener("mouseup", onUp);
|
|
130
|
+
};
|
|
131
|
+
}, [aspect, display]);
|
|
132
|
+
|
|
133
|
+
const apply = () => {
|
|
134
|
+
if (!crop || !natural || !display) return;
|
|
135
|
+
const sx = natural.w / display.w;
|
|
136
|
+
const sy = natural.h / display.h;
|
|
137
|
+
const outW = Math.max(1, Math.round(crop.w * sx));
|
|
138
|
+
const outH = Math.max(1, Math.round(crop.h * sy));
|
|
139
|
+
|
|
140
|
+
const img = new Image();
|
|
141
|
+
img.onload = () => {
|
|
142
|
+
const canvas = document.createElement("canvas");
|
|
143
|
+
canvas.width = outW;
|
|
144
|
+
canvas.height = outH;
|
|
145
|
+
const ctx = canvas.getContext("2d");
|
|
146
|
+
if (!ctx) return;
|
|
147
|
+
ctx.drawImage(
|
|
148
|
+
img,
|
|
149
|
+
crop.x * sx,
|
|
150
|
+
crop.y * sy,
|
|
151
|
+
crop.w * sx,
|
|
152
|
+
crop.h * sy,
|
|
153
|
+
0,
|
|
154
|
+
0,
|
|
155
|
+
outW,
|
|
156
|
+
outH,
|
|
157
|
+
);
|
|
158
|
+
onApply(canvas.toDataURL("image/png"));
|
|
159
|
+
};
|
|
160
|
+
img.src = image;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const cropPx =
|
|
164
|
+
crop && natural && display
|
|
165
|
+
? {
|
|
166
|
+
w: Math.round((crop.w / display.w) * natural.w),
|
|
167
|
+
h: Math.round((crop.h / display.h) * natural.h),
|
|
168
|
+
}
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="flex flex-col gap-6">
|
|
173
|
+
<div>
|
|
174
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
175
|
+
Aspect Ratio
|
|
176
|
+
</label>
|
|
177
|
+
<div className="flex flex-wrap gap-2">
|
|
178
|
+
{ASPECTS.map((a) => (
|
|
179
|
+
<button
|
|
180
|
+
key={a.label}
|
|
181
|
+
onClick={() => changeAspect(a.value)}
|
|
182
|
+
className={`px-3 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
183
|
+
aspect === a.value
|
|
184
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
185
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
186
|
+
}`}
|
|
187
|
+
>
|
|
188
|
+
{a.label}
|
|
189
|
+
</button>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="flex justify-center">
|
|
195
|
+
<div
|
|
196
|
+
ref={containerRef}
|
|
197
|
+
className="relative overflow-hidden rounded-xl border border-ink-200 bg-ink-100"
|
|
198
|
+
style={{ width: display?.w, height: display?.h }}
|
|
199
|
+
>
|
|
200
|
+
<img
|
|
201
|
+
src={image}
|
|
202
|
+
alt="crop"
|
|
203
|
+
draggable={false}
|
|
204
|
+
onLoad={handleLoad}
|
|
205
|
+
className="block select-none"
|
|
206
|
+
style={{ width: display?.w, height: display?.h }}
|
|
207
|
+
/>
|
|
208
|
+
{crop && display && (
|
|
209
|
+
<div
|
|
210
|
+
onMouseDown={startDrag("move")}
|
|
211
|
+
className="absolute border-2 border-ink-300 cursor-move"
|
|
212
|
+
style={{
|
|
213
|
+
left: crop.x,
|
|
214
|
+
top: crop.y,
|
|
215
|
+
width: crop.w,
|
|
216
|
+
height: crop.h,
|
|
217
|
+
boxShadow: "0 0 0 9999px rgba(0,0,0,0.5)",
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
onMouseDown={startDrag("resize")}
|
|
222
|
+
className="absolute right-0 bottom-0 w-4 h-4 bg-white border border-tiffany-500 rounded-md cursor-nwse-resize"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{cropPx && (
|
|
230
|
+
<p className="text-xs text-ink-600 text-center">
|
|
231
|
+
Crop size: {cropPx.w} × {cropPx.h}px
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
<div className="flex justify-end gap-2">
|
|
236
|
+
<button
|
|
237
|
+
onClick={onCancel}
|
|
238
|
+
className="px-4 py-2 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:bg-ink-100 transition-colors"
|
|
239
|
+
>
|
|
240
|
+
Cancel
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
onClick={apply}
|
|
244
|
+
className="px-4 py-2 text-xs font-medium rounded-xl bg-tiffany-500 hover:bg-tiffany-600 text-ink-950 transition-colors"
|
|
245
|
+
>
|
|
246
|
+
Apply Crop
|
|
247
|
+
</button>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { useEffect, useRef, useMemo, useState } from "react";
|
|
2
|
+
import { useGenerationStore } from "../../stores/generationStore";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
projectId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function ExtendVideoTab({ projectId }: Props) {
|
|
9
|
+
const store = useGenerationStore();
|
|
10
|
+
const extendLogRef = useRef<HTMLPreElement | any>(null);
|
|
11
|
+
const [search, setSearch] = useState("");
|
|
12
|
+
|
|
13
|
+
const filteredVideos = useMemo(() => {
|
|
14
|
+
if (!search.trim()) return store.projectVideos;
|
|
15
|
+
const q = search.toLowerCase();
|
|
16
|
+
return store.projectVideos.filter((v) =>
|
|
17
|
+
v.filename.toLowerCase().includes(q),
|
|
18
|
+
);
|
|
19
|
+
}, [store.projectVideos, search]);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (extendLogRef.current) {
|
|
23
|
+
extendLogRef.current.scrollTop = 100000;
|
|
24
|
+
}
|
|
25
|
+
}, [store.extend.logs]);
|
|
26
|
+
|
|
27
|
+
const handleGenerateExtend = async () => {
|
|
28
|
+
await store.generateExtend(
|
|
29
|
+
projectId,
|
|
30
|
+
store.selectedVideo?.url || store.selectedVideo?.filename || "",
|
|
31
|
+
);
|
|
32
|
+
document.body.scrollTop = 99999999999;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ========== SVG Icons ==========
|
|
36
|
+
|
|
37
|
+
const ExtendIcon = (
|
|
38
|
+
<svg
|
|
39
|
+
width="18"
|
|
40
|
+
height="18"
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
fill="none"
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="2"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
>
|
|
48
|
+
<polygon points="23 7 16 12 23 17 23 7" />
|
|
49
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
|
50
|
+
<line x1="5" y1="12" x2="11" y2="12" />
|
|
51
|
+
<line x1="8" y1="9" x2="8" y2="15" />
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const SparkleIcon = (
|
|
56
|
+
<svg
|
|
57
|
+
width="16"
|
|
58
|
+
height="16"
|
|
59
|
+
viewBox="0 0 24 24"
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="2"
|
|
63
|
+
>
|
|
64
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex flex-col gap-7">
|
|
70
|
+
<div className="flex items-center gap-2">
|
|
71
|
+
<span className="text-tiffany-600">{ExtendIcon}</span>
|
|
72
|
+
<h2 className="text-base font-semibold text-ink-900">
|
|
73
|
+
Extend Video
|
|
74
|
+
</h2>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Source video picker */}
|
|
78
|
+
<div className="flex gap-6">
|
|
79
|
+
{/* Video list */}
|
|
80
|
+
<div className="flex-1 min-w-0">
|
|
81
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
82
|
+
Source Video
|
|
83
|
+
</label>
|
|
84
|
+
{store.projectVideos.length > 0 && (
|
|
85
|
+
<div className="relative mb-2">
|
|
86
|
+
<svg
|
|
87
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 text-ink-500"
|
|
88
|
+
width="14"
|
|
89
|
+
height="14"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="currentColor"
|
|
93
|
+
strokeWidth="2"
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
>
|
|
97
|
+
<circle cx="11" cy="11" r="8" />
|
|
98
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
99
|
+
</svg>
|
|
100
|
+
<input
|
|
101
|
+
type="text"
|
|
102
|
+
placeholder="Filter videos..."
|
|
103
|
+
value={search}
|
|
104
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
105
|
+
className="w-full pl-9 pr-3 py-2 bg-ink-50 border border-ink-200 rounded-2xl text-ink-900 text-sm placeholder-ink-500 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
{store.projectVideosLoading ? (
|
|
110
|
+
<p className="text-xs text-ink-500 italic py-4">
|
|
111
|
+
Loading videos...
|
|
112
|
+
</p>
|
|
113
|
+
) : store.projectVideos.length === 0 ? (
|
|
114
|
+
<p className="text-xs text-ink-500 italic py-4 border border-dashed border-ink-200 rounded-2xl text-center">
|
|
115
|
+
No videos yet. Generate a video in the "Generate Video" tab.
|
|
116
|
+
</p>
|
|
117
|
+
) : filteredVideos.length === 0 ? (
|
|
118
|
+
<p className="text-xs text-ink-500 italic py-4 border border-dashed border-ink-200 rounded-2xl text-center">
|
|
119
|
+
No videos match "{search}".
|
|
120
|
+
</p>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="border border-ink-200 rounded-2xl overflow-hidden max-h-64 overflow-y-auto">
|
|
123
|
+
{filteredVideos.map((v) => {
|
|
124
|
+
const isSelected = store.selectedVideo?.filename === v.filename;
|
|
125
|
+
return (
|
|
126
|
+
<button
|
|
127
|
+
key={v.filename}
|
|
128
|
+
onClick={() => store.selectVideo(v)}
|
|
129
|
+
disabled={store.extend.generating}
|
|
130
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-left text-sm transition-colors border-b border-ink-200 last:border-b-0 ${
|
|
131
|
+
isSelected
|
|
132
|
+
? "bg-ink-100 text-ink-900 font-medium"
|
|
133
|
+
: "text-ink-700 hover:bg-ink-100"
|
|
134
|
+
} disabled:opacity-50`}
|
|
135
|
+
>
|
|
136
|
+
<svg
|
|
137
|
+
width="14"
|
|
138
|
+
height="14"
|
|
139
|
+
viewBox="0 0 24 24"
|
|
140
|
+
fill="none"
|
|
141
|
+
stroke="currentColor"
|
|
142
|
+
strokeWidth="2"
|
|
143
|
+
className="shrink-0 text-ink-500"
|
|
144
|
+
>
|
|
145
|
+
<polygon points="23 7 16 12 23 17 23 7" />
|
|
146
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
|
147
|
+
</svg>
|
|
148
|
+
<span className="truncate">{v.filename}</span>
|
|
149
|
+
</button>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Preview box */}
|
|
157
|
+
{store.selectedVideo && (
|
|
158
|
+
<div className="w-1/2 shrink-0">
|
|
159
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
160
|
+
Preview
|
|
161
|
+
</label>
|
|
162
|
+
<div className="relative rounded-2xl overflow-hidden border border-ink-200 bg-black aspect-video">
|
|
163
|
+
<video
|
|
164
|
+
src={
|
|
165
|
+
store.selectedVideo.url.startsWith("/")
|
|
166
|
+
? `http://localhost:${(window as any).PORT}${store.selectedVideo.url}`
|
|
167
|
+
: store.selectedVideo.url
|
|
168
|
+
}
|
|
169
|
+
muted={false}
|
|
170
|
+
playsInline
|
|
171
|
+
controls
|
|
172
|
+
className="w-full h-full object-contain"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<p className="text-[10px] text-tiffany-600 mt-1 truncate">
|
|
176
|
+
{store.selectedVideo.filename}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Prompt */}
|
|
183
|
+
<div>
|
|
184
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
185
|
+
Continue the Scene Prompt
|
|
186
|
+
</label>
|
|
187
|
+
<textarea
|
|
188
|
+
value={store.extend.prompt}
|
|
189
|
+
onChange={(e) => store.setExtendPrompt(e.target.value)}
|
|
190
|
+
placeholder="Describe how the video should continue, e.g. the camera holds, the motion continues naturally..."
|
|
191
|
+
rows={4}
|
|
192
|
+
disabled={store.extend.generating}
|
|
193
|
+
className="w-full px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl text-ink-900 text-sm placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-2 focus:ring-tiffany-500/30 transition-all resize-none disabled:opacity-50"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Duration (seconds) */}
|
|
198
|
+
<div>
|
|
199
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
200
|
+
Duration (seconds)
|
|
201
|
+
</label>
|
|
202
|
+
<div className="flex flex-wrap gap-2">
|
|
203
|
+
{[0.5, 1, 2, 3, 5, 7.5, 10].map((d) => (
|
|
204
|
+
<button
|
|
205
|
+
key={d}
|
|
206
|
+
onClick={() => store.setExtendDuration(d)}
|
|
207
|
+
disabled={store.extend.generating}
|
|
208
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
209
|
+
store.extend.extendDuration === d
|
|
210
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
211
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
212
|
+
} disabled:opacity-50`}
|
|
213
|
+
>
|
|
214
|
+
{d}s
|
|
215
|
+
</button>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
<p className="text-xs text-ink-600/50 mt-1.5">
|
|
219
|
+
{store.extend.extendFrames} frames ({store.extend.extendDuration}s ×
|
|
220
|
+
24 fps + 1)
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Generate / Stop button */}
|
|
225
|
+
{store.extend.generating ? (
|
|
226
|
+
<div className="flex items-center gap-3">
|
|
227
|
+
<div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
|
|
228
|
+
<svg
|
|
229
|
+
className="animate-spin text-tiffany-600"
|
|
230
|
+
width="16"
|
|
231
|
+
height="16"
|
|
232
|
+
viewBox="0 0 24 24"
|
|
233
|
+
fill="none"
|
|
234
|
+
stroke="currentColor"
|
|
235
|
+
strokeWidth="2"
|
|
236
|
+
>
|
|
237
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
238
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
|
239
|
+
</svg>
|
|
240
|
+
<span className="text-sm font-medium text-ink-700">
|
|
241
|
+
Extending...
|
|
242
|
+
</span>
|
|
243
|
+
</div>
|
|
244
|
+
<button
|
|
245
|
+
onClick={() => store.cancelGenerate()}
|
|
246
|
+
className="flex items-center justify-center gap-1.5 px-5 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"
|
|
247
|
+
>
|
|
248
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
249
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
250
|
+
</svg>
|
|
251
|
+
Stop
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
) : (
|
|
255
|
+
<button
|
|
256
|
+
onClick={handleGenerateExtend}
|
|
257
|
+
disabled={!store.selectedVideo || !store.extend.prompt.trim()}
|
|
258
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-tiffany-500 hover:bg-tiffany-600 active:bg-tiffany-700 disabled:bg-ink-200 disabled:text-ink-500 text-ink-950 text-sm font-semibold rounded-2xl transition-all duration-150 shadow-sm hover:shadow-md disabled:shadow-none"
|
|
259
|
+
>
|
|
260
|
+
{SparkleIcon}
|
|
261
|
+
Extend Video
|
|
262
|
+
</button>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Error */}
|
|
266
|
+
{store.extend.error && (
|
|
267
|
+
<div className="p-5 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
|
|
268
|
+
{store.extend.error}
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* Logs */}
|
|
273
|
+
{store.extend.logs.length > 0 && (
|
|
274
|
+
<div
|
|
275
|
+
ref={extendLogRef}
|
|
276
|
+
id="extend-logs"
|
|
277
|
+
className="p-5 bg-ink-50 border border-ink-200 rounded-2xl max-h-40 overflow-y-auto"
|
|
278
|
+
>
|
|
279
|
+
<p className="text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
280
|
+
Logs
|
|
281
|
+
</p>
|
|
282
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
283
|
+
{store.extend.logs.join("\n")}
|
|
284
|
+
</pre>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{/* Result */}
|
|
289
|
+
{store.extend.result && (
|
|
290
|
+
<div>
|
|
291
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
292
|
+
Extended Video
|
|
293
|
+
</label>
|
|
294
|
+
<div className="relative rounded-2xl overflow-hidden border border-ink-200 shadow-card bg-black w-[500px]">
|
|
295
|
+
<video
|
|
296
|
+
src={store.extend.result}
|
|
297
|
+
controls
|
|
298
|
+
className="w-full h-auto"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|