@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,722 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useGenerationStore } from "../../stores/generationStore";
|
|
3
|
+
import { useProjectStore } from "../../stores/projectStore";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
projectId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function GenerateVideoTab({ projectId }: Props) {
|
|
10
|
+
const store = useGenerationStore();
|
|
11
|
+
const { openFolder } = useProjectStore();
|
|
12
|
+
|
|
13
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
14
|
+
const csvInputRef = useRef<HTMLInputElement>(null);
|
|
15
|
+
const videoLogRef = useRef<HTMLPreElement | any>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (videoLogRef.current) {
|
|
19
|
+
videoLogRef.current.scrollTop = 100000;
|
|
20
|
+
}
|
|
21
|
+
}, [store.video.logs]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
let ttt = setInterval(() => {
|
|
25
|
+
let dom = document.querySelector("#video-logs");
|
|
26
|
+
if (dom) {
|
|
27
|
+
dom.scrollTop = 99999999;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
clearInterval(ttt);
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const handleGenerateVideo = async () => {
|
|
37
|
+
await store.generateVideo(projectId);
|
|
38
|
+
document.body.scrollTop = 99999999999;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleBatchGenerate = async () => {
|
|
42
|
+
await store.generateBatchVideos(projectId);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleOpenVideoFolder = async () => {
|
|
46
|
+
await openFolder(projectId, "output");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
50
|
+
const file = e.target.files?.[0];
|
|
51
|
+
if (!file) return;
|
|
52
|
+
|
|
53
|
+
// Read file as base64 data URL
|
|
54
|
+
const reader = new FileReader();
|
|
55
|
+
reader.onload = async () => {
|
|
56
|
+
const base64 = reader.result as string;
|
|
57
|
+
const uploadedPath = await store.uploadImage(
|
|
58
|
+
projectId,
|
|
59
|
+
base64,
|
|
60
|
+
file.name,
|
|
61
|
+
);
|
|
62
|
+
if (uploadedPath) {
|
|
63
|
+
// Refresh the project images list so the new upload appears
|
|
64
|
+
store.fetchProjectImages(projectId);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
reader.readAsDataURL(file);
|
|
68
|
+
|
|
69
|
+
// Reset so the same file can be re-selected
|
|
70
|
+
e.target.value = "";
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleCsvSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
74
|
+
const file = e.target.files?.[0];
|
|
75
|
+
if (!file) return;
|
|
76
|
+
|
|
77
|
+
const reader = new FileReader();
|
|
78
|
+
reader.onload = () => {
|
|
79
|
+
const base64 = reader.result as string;
|
|
80
|
+
store.uploadCsv(base64, file.name);
|
|
81
|
+
};
|
|
82
|
+
reader.readAsDataURL(file);
|
|
83
|
+
|
|
84
|
+
e.target.value = "";
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ========== SVG Icons ==========
|
|
88
|
+
|
|
89
|
+
const VideoIcon = (
|
|
90
|
+
<svg
|
|
91
|
+
width="18"
|
|
92
|
+
height="18"
|
|
93
|
+
viewBox="0 0 24 24"
|
|
94
|
+
fill="none"
|
|
95
|
+
stroke="currentColor"
|
|
96
|
+
strokeWidth="2"
|
|
97
|
+
>
|
|
98
|
+
<polygon points="23 7 16 12 23 17 23 7" />
|
|
99
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
|
100
|
+
</svg>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const SparkleIcon = (
|
|
104
|
+
<svg
|
|
105
|
+
width="16"
|
|
106
|
+
height="16"
|
|
107
|
+
viewBox="0 0 24 24"
|
|
108
|
+
fill="none"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
strokeWidth="2"
|
|
111
|
+
>
|
|
112
|
+
<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" />
|
|
113
|
+
</svg>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const FolderIcon = (
|
|
117
|
+
<svg
|
|
118
|
+
width="16"
|
|
119
|
+
height="16"
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
fill="none"
|
|
122
|
+
stroke="currentColor"
|
|
123
|
+
strokeWidth="2"
|
|
124
|
+
strokeLinecap="round"
|
|
125
|
+
strokeLinejoin="round"
|
|
126
|
+
>
|
|
127
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
128
|
+
</svg>
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const CsvIcon = (
|
|
132
|
+
<svg
|
|
133
|
+
width="16"
|
|
134
|
+
height="16"
|
|
135
|
+
viewBox="0 0 24 24"
|
|
136
|
+
fill="none"
|
|
137
|
+
stroke="currentColor"
|
|
138
|
+
strokeWidth="2"
|
|
139
|
+
strokeLinecap="round"
|
|
140
|
+
strokeLinejoin="round"
|
|
141
|
+
>
|
|
142
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
143
|
+
<polyline points="14 2 14 8 20 8" />
|
|
144
|
+
<line x1="8" y1="13" x2="16" y2="13" />
|
|
145
|
+
<line x1="8" y1="17" x2="16" y2="17" />
|
|
146
|
+
<line x1="8" y1="9" x2="10" y2="9" />
|
|
147
|
+
</svg>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const TableIcon = (
|
|
151
|
+
<svg
|
|
152
|
+
width="14"
|
|
153
|
+
height="14"
|
|
154
|
+
viewBox="0 0 24 24"
|
|
155
|
+
fill="none"
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
strokeWidth="2"
|
|
158
|
+
strokeLinecap="round"
|
|
159
|
+
strokeLinejoin="round"
|
|
160
|
+
>
|
|
161
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
162
|
+
<line x1="3" y1="9" x2="21" y2="9" />
|
|
163
|
+
<line x1="3" y1="15" x2="21" y2="15" />
|
|
164
|
+
<line x1="9" y1="3" x2="9" y2="21" />
|
|
165
|
+
</svg>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const DownloadIcon = (
|
|
169
|
+
<svg
|
|
170
|
+
width="14"
|
|
171
|
+
height="14"
|
|
172
|
+
viewBox="0 0 24 24"
|
|
173
|
+
fill="none"
|
|
174
|
+
stroke="currentColor"
|
|
175
|
+
strokeWidth="2"
|
|
176
|
+
strokeLinecap="round"
|
|
177
|
+
strokeLinejoin="round"
|
|
178
|
+
>
|
|
179
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
180
|
+
<polyline points="7 10 12 15 17 10" />
|
|
181
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
182
|
+
</svg>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex flex-col gap-7">
|
|
187
|
+
<div className="flex items-center gap-2">
|
|
188
|
+
<span className="text-tiffany-600">{VideoIcon}</span>
|
|
189
|
+
<h2 className="text-base font-semibold text-ink-900">
|
|
190
|
+
Scene Video Generation
|
|
191
|
+
</h2>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* CSV upload for batch generation */}
|
|
195
|
+
<div>
|
|
196
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
197
|
+
CSV Batch Data
|
|
198
|
+
</label>
|
|
199
|
+
<div className="flex items-center gap-2">
|
|
200
|
+
<input
|
|
201
|
+
ref={csvInputRef}
|
|
202
|
+
type="file"
|
|
203
|
+
accept=".csv"
|
|
204
|
+
onChange={handleCsvSelect}
|
|
205
|
+
disabled={store.batchRunning}
|
|
206
|
+
className="flex-1 text-sm text-ink-700 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-sm file:font-medium file:bg-ink-100 file:text-ink-700 hover:file:bg-ink-200 file:cursor-pointer file:transition-colors disabled:opacity-50"
|
|
207
|
+
/>
|
|
208
|
+
{store.csvFilename && (
|
|
209
|
+
<button
|
|
210
|
+
onClick={() => store.clearCsvData()}
|
|
211
|
+
disabled={store.batchRunning}
|
|
212
|
+
className="px-3 py-2 text-xs font-medium text-red-600 hover:bg-red-50 rounded-xl transition-colors disabled:opacity-50"
|
|
213
|
+
>
|
|
214
|
+
Clear
|
|
215
|
+
</button>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
<a
|
|
219
|
+
href={(() => {
|
|
220
|
+
const csv = `id,book,chapter,verses,script
|
|
221
|
+
1,Ephesians,4,32,"Be kind to one another, tenderhearted, forgiving one another, as God in Christ forgave you."
|
|
222
|
+
2,Colossians,3,13,Bear with each other and forgive one another if any of you has a grievance against someone. Forgive as the Lord forgave you.
|
|
223
|
+
3,Matthew,6,14,"For if you forgive other people when they sin against you, your heavenly Father will also forgive you."
|
|
224
|
+
4,Luke,6,37,"Do not judge, and you will not be judged. Do not condemn, and you will not be condemned. Forgive, and you will be forgiven."
|
|
225
|
+
5,Mark,11,25,"And whenever you stand praying, forgive, if you have anything against anyone, so that your Father also who is in heaven may forgive you."
|
|
226
|
+
6,1 John,1,9,"If we confess our sins, he is faithful and just to forgive us our sins and to cleanse us from all unrighteousness."
|
|
227
|
+
7,Psalm,103,12,"As far as the east is from the west, so far has he removed our transgressions from us."
|
|
228
|
+
8,Proverbs,17,9,"Whoever would foster love covers over an offense, but whoever repeats the matter separates close friends."
|
|
229
|
+
9,Psalm,86,5,"For you, O Lord, are good and forgiving, abounding in steadfast love to all who call upon you."
|
|
230
|
+
10,Isaiah,43,25,"I, even I, am he who blots out your transgressions, for my own sake, and remembers your sins no more."`;
|
|
231
|
+
return `data:text/csv;charset=utf-8,${encodeURIComponent(csv.trim())}`;
|
|
232
|
+
})()}
|
|
233
|
+
download="bible.csv"
|
|
234
|
+
className="inline-flex items-center gap-1.5 mt-2 text-xs font-medium text-ink-600 hover:text-ink-900 transition-colors"
|
|
235
|
+
>
|
|
236
|
+
{DownloadIcon}
|
|
237
|
+
Download example CSV file
|
|
238
|
+
</a>
|
|
239
|
+
{/* */}
|
|
240
|
+
{store.csvFilename && (
|
|
241
|
+
<div className="mt-2 border border-ink-200 rounded-2xl overflow-hidden">
|
|
242
|
+
{/* CSV header */}
|
|
243
|
+
<div className="flex items-center justify-between px-4 py-2.5 bg-ink-50 border-b border-ink-200">
|
|
244
|
+
<div className="flex items-center gap-2 text-xs text-ink-700">
|
|
245
|
+
{CsvIcon}
|
|
246
|
+
<span className="font-medium">{store.csvFilename}</span>
|
|
247
|
+
<span className="text-ink-600/60">
|
|
248
|
+
({store.csvRows.length} rows, {store.csvColumns.length}{" "}
|
|
249
|
+
columns)
|
|
250
|
+
</span>
|
|
251
|
+
<span className="text-ink-600/60">
|
|
252
|
+
— {store.csvSelectedIndices.size} selected
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="flex items-center gap-1.5">
|
|
256
|
+
<button
|
|
257
|
+
onClick={() => store.selectAllCsvRows()}
|
|
258
|
+
disabled={store.batchRunning}
|
|
259
|
+
className="px-2 py-1 text-[10px] font-medium text-ink-600 hover:bg-ink-200 rounded transition-colors disabled:opacity-50"
|
|
260
|
+
>
|
|
261
|
+
Select All
|
|
262
|
+
</button>
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => store.deselectAllCsvRows()}
|
|
265
|
+
disabled={store.batchRunning}
|
|
266
|
+
className="px-2 py-1 text-[10px] font-medium text-ink-600 hover:bg-ink-200 rounded transition-colors disabled:opacity-50"
|
|
267
|
+
>
|
|
268
|
+
Deselect All
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Table container with horizontal scroll */}
|
|
274
|
+
<div className="overflow-auto max-h-80">
|
|
275
|
+
<table className="w-full text-xs">
|
|
276
|
+
{/* Column headers */}
|
|
277
|
+
<thead>
|
|
278
|
+
<tr className="bg-ink-100/70 border-b border-ink-200">
|
|
279
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 w-10">
|
|
280
|
+
<span className="sr-only">Select</span>
|
|
281
|
+
</th>
|
|
282
|
+
{store.csvColumns.map((col) => (
|
|
283
|
+
<th
|
|
284
|
+
key={col}
|
|
285
|
+
className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 whitespace-nowrap"
|
|
286
|
+
>
|
|
287
|
+
{col}
|
|
288
|
+
<span className="ml-1 text-[10px] font-normal text-ink-500">{`{{${col}}}`}</span>
|
|
289
|
+
</th>
|
|
290
|
+
))}
|
|
291
|
+
</tr>
|
|
292
|
+
</thead>
|
|
293
|
+
<tbody>
|
|
294
|
+
{store.csvRows.map((row, rowIdx) => {
|
|
295
|
+
const isSelected = store.csvSelectedIndices.has(rowIdx);
|
|
296
|
+
return (
|
|
297
|
+
<tr
|
|
298
|
+
key={rowIdx}
|
|
299
|
+
className={`border-b border-ink-200 transition-colors ${
|
|
300
|
+
isSelected
|
|
301
|
+
? "bg-white"
|
|
302
|
+
: "bg-ink-100/50 opacity-60"
|
|
303
|
+
}`}
|
|
304
|
+
>
|
|
305
|
+
<td className="px-3 py-1.5">
|
|
306
|
+
<input
|
|
307
|
+
type="checkbox"
|
|
308
|
+
checked={isSelected}
|
|
309
|
+
onChange={() => store.toggleCsvRow(rowIdx)}
|
|
310
|
+
disabled={store.batchRunning}
|
|
311
|
+
className="w-3.5 h-3.5 rounded border-ink-300 text-ink-600 focus:ring-tiffany-500 cursor-pointer disabled:opacity-50"
|
|
312
|
+
/>
|
|
313
|
+
</td>
|
|
314
|
+
{store.csvColumns.map((col) => (
|
|
315
|
+
<td key={col} className="px-3 py-1">
|
|
316
|
+
<input
|
|
317
|
+
type="text"
|
|
318
|
+
value={row[col] ?? ""}
|
|
319
|
+
onChange={(e) =>
|
|
320
|
+
store.updateCsvCell(rowIdx, col, e.target.value)
|
|
321
|
+
}
|
|
322
|
+
disabled={store.batchRunning}
|
|
323
|
+
className="w-full min-w-[120px] px-2 py-1 bg-transparent border border-transparent hover:border-ink-400 focus:border-tiffany-500 focus:outline-none focus:ring-1 focus:ring-tiffany-500/30 rounded text-ink-800 transition-colors disabled:opacity-50"
|
|
324
|
+
/>
|
|
325
|
+
</td>
|
|
326
|
+
))}
|
|
327
|
+
</tr>
|
|
328
|
+
);
|
|
329
|
+
})}
|
|
330
|
+
</tbody>
|
|
331
|
+
</table>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
{!store.csvFilename && (
|
|
336
|
+
<p className="text-xs text-ink-600/50 mt-1.5">
|
|
337
|
+
Upload a CSV with a{" "}
|
|
338
|
+
<code className="text-[11px] bg-ink-100 px-1 rounded">
|
|
339
|
+
name
|
|
340
|
+
</code>{" "}
|
|
341
|
+
column to batch-generate many videos. Use{" "}
|
|
342
|
+
<code className="text-[11px] bg-ink-100 px-1 rounded">{`{{column}}`}</code>{" "}
|
|
343
|
+
in your prompt as a template.
|
|
344
|
+
</p>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
{/* */}
|
|
348
|
+
{/* Image upload */}
|
|
349
|
+
<div>
|
|
350
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
351
|
+
Input Image
|
|
352
|
+
</label>
|
|
353
|
+
<input
|
|
354
|
+
ref={fileInputRef}
|
|
355
|
+
type="file"
|
|
356
|
+
accept="image/*"
|
|
357
|
+
onChange={handleFileSelect}
|
|
358
|
+
className="w-full text-sm text-ink-700 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-sm file:font-medium file:bg-ink-100 file:text-ink-700 hover:file:bg-ink-200 file:cursor-pointer file:transition-colors"
|
|
359
|
+
/>
|
|
360
|
+
<p className="text-xs text-ink-600/50 mt-1.5">
|
|
361
|
+
Upload an image to animate.
|
|
362
|
+
</p>
|
|
363
|
+
|
|
364
|
+
<a
|
|
365
|
+
href={`/lambobo.png.zip`}
|
|
366
|
+
download="lambobo.zip"
|
|
367
|
+
className="inline-flex items-center gap-1.5 mt-2 text-xs font-medium text-ink-600 hover:text-ink-900 transition-colors"
|
|
368
|
+
>
|
|
369
|
+
{DownloadIcon}
|
|
370
|
+
Download example image file
|
|
371
|
+
</a>
|
|
372
|
+
<br />
|
|
373
|
+
|
|
374
|
+
{store.uploading && (
|
|
375
|
+
<p className="text-xs text-ink-600 mt-1">Uploading...</p>
|
|
376
|
+
)}
|
|
377
|
+
{store.uploadError && (
|
|
378
|
+
<p className="text-xs text-red-600 mt-1">{store.uploadError}</p>
|
|
379
|
+
)}
|
|
380
|
+
{store.uploadedImageUrl && (
|
|
381
|
+
<div className="mt-3 rounded-2xl overflow-hidden border border-ink-200 bg-ink-100 inline-block">
|
|
382
|
+
<img
|
|
383
|
+
src={store.uploadedImageUrl}
|
|
384
|
+
alt={store.uploadedImageFilename || "Uploaded image"}
|
|
385
|
+
className="w-full h-32 object-cover"
|
|
386
|
+
/>
|
|
387
|
+
{store.uploadedImageFilename && (
|
|
388
|
+
<p className="px-3 py-1.5 text-xs text-ink-600 truncate">
|
|
389
|
+
{store.uploadedImageFilename}
|
|
390
|
+
</p>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
{/* Project images picker */}
|
|
397
|
+
<div>
|
|
398
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
399
|
+
Project Images
|
|
400
|
+
</label>
|
|
401
|
+
{store.projectImagesLoading ? (
|
|
402
|
+
<p className="text-xs text-ink-500 italic py-4 text-center">
|
|
403
|
+
Loading images...
|
|
404
|
+
</p>
|
|
405
|
+
) : store.projectImages.length === 0 ? (
|
|
406
|
+
<p className="text-xs text-ink-500 italic py-4 text-center border border-dashed border-ink-200 rounded-2xl">
|
|
407
|
+
No images yet. Upload one above.
|
|
408
|
+
</p>
|
|
409
|
+
) : (
|
|
410
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 3xl:grid-cols-10 gap-2 p-1">
|
|
411
|
+
{store.projectImages.map((img) => {
|
|
412
|
+
const isSelected =
|
|
413
|
+
store.selectedImage?.filename === img.filename &&
|
|
414
|
+
store.selectedImage?.source === img.source;
|
|
415
|
+
|
|
416
|
+
const fullUrl = img.url.startsWith("http")
|
|
417
|
+
? img.url
|
|
418
|
+
: `http://localhost:${(window as any).PORT}${img.url}`;
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<button
|
|
422
|
+
key={`${img.source}-${img.filename}`}
|
|
423
|
+
onClick={() => store.selectImage(img)}
|
|
424
|
+
disabled={store.video.generating}
|
|
425
|
+
className={`relative rounded-xl border-2 transition-all ${
|
|
426
|
+
isSelected
|
|
427
|
+
? "border-tiffany-500 ring-2 ring-tiffany-500/40"
|
|
428
|
+
: "border-ink-200 hover:border-ink-300"
|
|
429
|
+
} disabled:opacity-50`}
|
|
430
|
+
>
|
|
431
|
+
<img
|
|
432
|
+
src={`${fullUrl}`}
|
|
433
|
+
alt={img.filename}
|
|
434
|
+
className="aspect-square object-cover object-center"
|
|
435
|
+
/>
|
|
436
|
+
<span className="absolute bottom-0 left-0 right-0 bg-white/80 backdrop-blur-sm px-1.5 py-0.5 text-[10px] text-ink-700 truncate text-center">
|
|
437
|
+
{img.source === "generated" ? "✦ " : "↑ "}
|
|
438
|
+
{img.filename}
|
|
439
|
+
</span>
|
|
440
|
+
</button>
|
|
441
|
+
);
|
|
442
|
+
})}
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
{store.selectedImage && (
|
|
446
|
+
<p className="text-xs text-ink-600/60 mt-1.5">
|
|
447
|
+
Selected:{" "}
|
|
448
|
+
<span className="font-medium text-ink-700">
|
|
449
|
+
{store.selectedImage.filename}
|
|
450
|
+
</span>
|
|
451
|
+
</p>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
{/* Prompt */}
|
|
456
|
+
<div>
|
|
457
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
458
|
+
Scene Prompt
|
|
459
|
+
{store.csvColumns.length > 0 && (
|
|
460
|
+
<span className="ml-2 text-[10px] font-normal text-tiffany-600">
|
|
461
|
+
— template with {`{{column}}`} placeholders
|
|
462
|
+
</span>
|
|
463
|
+
)}
|
|
464
|
+
</label>
|
|
465
|
+
<textarea
|
|
466
|
+
value={store.video.prompt}
|
|
467
|
+
onChange={(e) => store.setVideoPrompt(e.target.value)}
|
|
468
|
+
placeholder={
|
|
469
|
+
store.csvColumns.length > 0
|
|
470
|
+
? `Use {{name}} and other column placeholders, e.g. a {{name}} says: "Hi John Wayne, how are you? my name is {{name}}."`
|
|
471
|
+
: "Describe the video scene, e.g. a gentle ocean wave rolling onto a sandy beach at golden hour..."
|
|
472
|
+
}
|
|
473
|
+
rows={4}
|
|
474
|
+
disabled={store.video.generating || store.batchRunning}
|
|
475
|
+
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"
|
|
476
|
+
/>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
{/* Duration selector */}
|
|
480
|
+
<div>
|
|
481
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
482
|
+
Duration (seconds)
|
|
483
|
+
</label>
|
|
484
|
+
<div className="flex flex-wrap gap-2">
|
|
485
|
+
{[0.5, 3, 5, 7.5, 10, 12.5, 15, 17.5, 20].map((d) => (
|
|
486
|
+
<button
|
|
487
|
+
key={d}
|
|
488
|
+
onClick={() => store.setVideoDuration(d)}
|
|
489
|
+
disabled={store.video.generating}
|
|
490
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
491
|
+
store.video.duration === d
|
|
492
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
493
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
494
|
+
} disabled:opacity-50`}
|
|
495
|
+
>
|
|
496
|
+
{d}s
|
|
497
|
+
</button>
|
|
498
|
+
))}
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
{/* Aspect Ratio selector */}
|
|
503
|
+
<div>
|
|
504
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
505
|
+
Aspect Ratio
|
|
506
|
+
</label>
|
|
507
|
+
<div className="flex flex-wrap gap-2">
|
|
508
|
+
{(["1:1", "16:9", "9:16", "4:3", "3:4"] as const).map((ratio) => (
|
|
509
|
+
<button
|
|
510
|
+
key={ratio}
|
|
511
|
+
onClick={() => store.setVideoAspectRatio(ratio)}
|
|
512
|
+
disabled={store.video.generating}
|
|
513
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
514
|
+
store.video.aspectRatio === ratio
|
|
515
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
516
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
517
|
+
} disabled:opacity-50`}
|
|
518
|
+
>
|
|
519
|
+
{ratio}
|
|
520
|
+
</button>
|
|
521
|
+
))}
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
{/* Resolution selector */}
|
|
526
|
+
<div>
|
|
527
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
528
|
+
Resolution
|
|
529
|
+
</label>
|
|
530
|
+
<div className="flex flex-wrap gap-2">
|
|
531
|
+
{(["320p", "480p", "576p", "640p", "720p", "1080p"] as const).map(
|
|
532
|
+
(res: any) => (
|
|
533
|
+
<button
|
|
534
|
+
key={res}
|
|
535
|
+
onClick={() => store.setVideoResolution(res)}
|
|
536
|
+
disabled={store.video.generating}
|
|
537
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
538
|
+
store.video.resolution === res
|
|
539
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
540
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
541
|
+
} disabled:opacity-50`}
|
|
542
|
+
>
|
|
543
|
+
{res}
|
|
544
|
+
</button>
|
|
545
|
+
),
|
|
546
|
+
)}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
{/* Quality selector */}
|
|
551
|
+
<div>
|
|
552
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
553
|
+
Quality
|
|
554
|
+
</label>
|
|
555
|
+
<div className="flex flex-wrap gap-2">
|
|
556
|
+
{(
|
|
557
|
+
[
|
|
558
|
+
{ label: "Low (distilled)", value: "distilled" },
|
|
559
|
+
{ label: "Standard (one-stage)", value: "one-stage" },
|
|
560
|
+
{ label: "High (two-stage)", value: "two-stage" },
|
|
561
|
+
] as const
|
|
562
|
+
).map((q) => (
|
|
563
|
+
<button
|
|
564
|
+
key={q.value}
|
|
565
|
+
onClick={() => store.setVideoMode(q.value)}
|
|
566
|
+
disabled={store.video.generating}
|
|
567
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
568
|
+
store.video.mode === q.value
|
|
569
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
570
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
571
|
+
} disabled:opacity-50`}
|
|
572
|
+
>
|
|
573
|
+
{q.label}
|
|
574
|
+
</button>
|
|
575
|
+
))}
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<button
|
|
580
|
+
onClick={handleOpenVideoFolder}
|
|
581
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 mb-3 bg-ink-50 hover:bg-ink-200 text-ink-700 text-sm font-medium rounded-2xl border border-ink-200 transition-colors"
|
|
582
|
+
>
|
|
583
|
+
{FolderIcon}
|
|
584
|
+
Open Output Folder
|
|
585
|
+
</button>
|
|
586
|
+
|
|
587
|
+
{/* Batch generate button */}
|
|
588
|
+
{store.csvRows.length > 0 && (
|
|
589
|
+
<div className="space-y-2">
|
|
590
|
+
{store.batchProgress && (
|
|
591
|
+
<div className="flex items-center gap-3 p-4 bg-ink-50 border border-ink-200 rounded-2xl">
|
|
592
|
+
<span className="text-xs font-medium text-ink-700">
|
|
593
|
+
{store.batchRunning ? "Batch Progress" : "Batch Complete"}
|
|
594
|
+
</span>
|
|
595
|
+
<div className="flex-1 h-2 bg-ink-200 rounded-full overflow-hidden">
|
|
596
|
+
<div
|
|
597
|
+
className="h-full bg-tiffany-500 rounded-full transition-all duration-300"
|
|
598
|
+
style={{
|
|
599
|
+
width: `${(store.batchProgress.current / store.batchProgress.total) * 100}%`,
|
|
600
|
+
}}
|
|
601
|
+
/>
|
|
602
|
+
</div>
|
|
603
|
+
<span className="text-xs font-semibold text-ink-700 tabular-nums">
|
|
604
|
+
{store.batchProgress.current}/{store.batchProgress.total}
|
|
605
|
+
</span>
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
{store.batchRunning ? (
|
|
609
|
+
<button
|
|
610
|
+
onClick={() => {
|
|
611
|
+
store.cancelBatch();
|
|
612
|
+
store.cancelGenerate();
|
|
613
|
+
}}
|
|
614
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-3 bg-red-50 hover:bg-red-100 text-red-600 text-sm font-semibold rounded-2xl border border-red-200 transition-colors"
|
|
615
|
+
>
|
|
616
|
+
Cancel Batch
|
|
617
|
+
</button>
|
|
618
|
+
) : (
|
|
619
|
+
<button
|
|
620
|
+
onClick={handleBatchGenerate}
|
|
621
|
+
disabled={
|
|
622
|
+
store.video.generating ||
|
|
623
|
+
!store.video.prompt.trim() ||
|
|
624
|
+
store.uploading
|
|
625
|
+
}
|
|
626
|
+
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"
|
|
627
|
+
>
|
|
628
|
+
{TableIcon}
|
|
629
|
+
Generate Batch ({store.csvSelectedIndices.size} videos)
|
|
630
|
+
</button>
|
|
631
|
+
)}
|
|
632
|
+
</div>
|
|
633
|
+
)}
|
|
634
|
+
|
|
635
|
+
{/* Generate / Stop button (video) */}
|
|
636
|
+
{store.video.generating ? (
|
|
637
|
+
<div className="flex items-center gap-3">
|
|
638
|
+
<div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
|
|
639
|
+
<svg
|
|
640
|
+
className="animate-spin text-tiffany-600"
|
|
641
|
+
width="16"
|
|
642
|
+
height="16"
|
|
643
|
+
viewBox="0 0 24 24"
|
|
644
|
+
fill="none"
|
|
645
|
+
stroke="currentColor"
|
|
646
|
+
strokeWidth="2"
|
|
647
|
+
>
|
|
648
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
649
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
|
650
|
+
</svg>
|
|
651
|
+
<span className="text-sm font-medium text-ink-700">
|
|
652
|
+
Generating...
|
|
653
|
+
</span>
|
|
654
|
+
</div>
|
|
655
|
+
<button
|
|
656
|
+
onClick={() => {
|
|
657
|
+
store.cancelBatch();
|
|
658
|
+
store.cancelGenerate();
|
|
659
|
+
}}
|
|
660
|
+
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"
|
|
661
|
+
>
|
|
662
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
663
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
664
|
+
</svg>
|
|
665
|
+
Stop
|
|
666
|
+
</button>
|
|
667
|
+
</div>
|
|
668
|
+
) : (
|
|
669
|
+
<button
|
|
670
|
+
onClick={handleGenerateVideo}
|
|
671
|
+
disabled={
|
|
672
|
+
store.batchRunning || !store.video.prompt.trim() || store.uploading
|
|
673
|
+
}
|
|
674
|
+
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"
|
|
675
|
+
>
|
|
676
|
+
{SparkleIcon}
|
|
677
|
+
{`Generate 1 Video (No CSV / Template support)`}
|
|
678
|
+
</button>
|
|
679
|
+
)}
|
|
680
|
+
|
|
681
|
+
{/* Error */}
|
|
682
|
+
{store.video.error && (
|
|
683
|
+
<div className="p-5 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
|
|
684
|
+
{store.video.error}
|
|
685
|
+
</div>
|
|
686
|
+
)}
|
|
687
|
+
|
|
688
|
+
{/* Logs */}
|
|
689
|
+
{store.video.logs.length > 0 && (
|
|
690
|
+
<div
|
|
691
|
+
ref={videoLogRef}
|
|
692
|
+
id="video-logs"
|
|
693
|
+
className="p-5 bg-ink-50 border border-ink-200 rounded-2xl max-h-40 overflow-y-auto"
|
|
694
|
+
>
|
|
695
|
+
<p className="text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
696
|
+
Logs
|
|
697
|
+
</p>
|
|
698
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
699
|
+
{store.video.logs.join("\n")}
|
|
700
|
+
</pre>
|
|
701
|
+
</div>
|
|
702
|
+
)}
|
|
703
|
+
|
|
704
|
+
{/* Result */}
|
|
705
|
+
{store.video.result && (
|
|
706
|
+
<div>
|
|
707
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
708
|
+
Generated Video
|
|
709
|
+
</label>
|
|
710
|
+
|
|
711
|
+
<div className="relative rounded-2xl overflow-hidden border border-ink-200 shadow-card bg-black w-[500px]">
|
|
712
|
+
<video
|
|
713
|
+
src={store.video.result}
|
|
714
|
+
controls
|
|
715
|
+
className="w-full h-auto"
|
|
716
|
+
/>
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
)}
|
|
720
|
+
</div>
|
|
721
|
+
);
|
|
722
|
+
}
|