@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,927 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useBatchImageToVideoStore,
|
|
4
|
+
type BatchI2VRowStatus,
|
|
5
|
+
} from "../../stores/batchImageToVideoStore";
|
|
6
|
+
import { useProjectStore } from "../../stores/projectStore";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
projectId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ========== SVG Icons ==========
|
|
13
|
+
|
|
14
|
+
const TableIcon = (
|
|
15
|
+
<svg
|
|
16
|
+
width="18"
|
|
17
|
+
height="18"
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
fill="none"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
strokeWidth="2"
|
|
22
|
+
strokeLinecap="round"
|
|
23
|
+
strokeLinejoin="round"
|
|
24
|
+
className="text-tiffany-600"
|
|
25
|
+
>
|
|
26
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
27
|
+
<line x1="3" y1="9" x2="21" y2="9" />
|
|
28
|
+
<line x1="3" y1="15" x2="21" y2="15" />
|
|
29
|
+
<line x1="9" y1="3" x2="9" y2="21" />
|
|
30
|
+
</svg>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const PlusIcon = (
|
|
34
|
+
<svg
|
|
35
|
+
width="14"
|
|
36
|
+
height="14"
|
|
37
|
+
viewBox="0 0 24 24"
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="currentColor"
|
|
40
|
+
strokeWidth="2"
|
|
41
|
+
strokeLinecap="round"
|
|
42
|
+
strokeLinejoin="round"
|
|
43
|
+
>
|
|
44
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
45
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const UploadIcon = (
|
|
50
|
+
<svg
|
|
51
|
+
width="14"
|
|
52
|
+
height="14"
|
|
53
|
+
viewBox="0 0 24 24"
|
|
54
|
+
fill="none"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
strokeWidth="2"
|
|
57
|
+
strokeLinecap="round"
|
|
58
|
+
strokeLinejoin="round"
|
|
59
|
+
>
|
|
60
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
61
|
+
<polyline points="17 8 12 3 7 8" />
|
|
62
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const ClipboardIcon = (
|
|
67
|
+
<svg
|
|
68
|
+
width="14"
|
|
69
|
+
height="14"
|
|
70
|
+
viewBox="0 0 24 24"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
strokeWidth="2"
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
>
|
|
77
|
+
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
|
|
78
|
+
<rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const FileIcon = (
|
|
83
|
+
<svg
|
|
84
|
+
width="14"
|
|
85
|
+
height="14"
|
|
86
|
+
viewBox="0 0 24 24"
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
strokeWidth="2"
|
|
90
|
+
strokeLinecap="round"
|
|
91
|
+
strokeLinejoin="round"
|
|
92
|
+
>
|
|
93
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
94
|
+
<polyline points="14 2 14 8 20 8" />
|
|
95
|
+
</svg>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const ImageIcon = (
|
|
99
|
+
<svg
|
|
100
|
+
width="16"
|
|
101
|
+
height="16"
|
|
102
|
+
viewBox="0 0 24 24"
|
|
103
|
+
fill="none"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
strokeWidth="2"
|
|
106
|
+
strokeLinecap="round"
|
|
107
|
+
strokeLinejoin="round"
|
|
108
|
+
>
|
|
109
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
110
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
111
|
+
<polyline points="21 15 16 10 5 21" />
|
|
112
|
+
</svg>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const FilmIcon = (
|
|
116
|
+
<svg
|
|
117
|
+
width="16"
|
|
118
|
+
height="16"
|
|
119
|
+
viewBox="0 0 24 24"
|
|
120
|
+
fill="none"
|
|
121
|
+
stroke="currentColor"
|
|
122
|
+
strokeWidth="2"
|
|
123
|
+
strokeLinecap="round"
|
|
124
|
+
strokeLinejoin="round"
|
|
125
|
+
>
|
|
126
|
+
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18" />
|
|
127
|
+
<line x1="7" y1="2" x2="7" y2="22" />
|
|
128
|
+
<line x1="17" y1="2" x2="17" y2="22" />
|
|
129
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
130
|
+
<line x1="2" y1="7" x2="7" y2="7" />
|
|
131
|
+
<line x1="2" y1="17" x2="7" y2="17" />
|
|
132
|
+
<line x1="17" y1="17" x2="22" y2="17" />
|
|
133
|
+
<line x1="17" y1="7" x2="22" y2="7" />
|
|
134
|
+
</svg>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const ExpandIcon = (
|
|
138
|
+
<svg
|
|
139
|
+
width="14"
|
|
140
|
+
height="14"
|
|
141
|
+
viewBox="0 0 24 24"
|
|
142
|
+
fill="none"
|
|
143
|
+
stroke="currentColor"
|
|
144
|
+
strokeWidth="2"
|
|
145
|
+
strokeLinecap="round"
|
|
146
|
+
strokeLinejoin="round"
|
|
147
|
+
>
|
|
148
|
+
<polyline points="15 3 21 3 21 9" />
|
|
149
|
+
<polyline points="9 21 3 21 3 15" />
|
|
150
|
+
<line x1="21" y1="3" x2="14" y2="10" />
|
|
151
|
+
<line x1="3" y1="21" x2="10" y2="14" />
|
|
152
|
+
</svg>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const FolderIcon = (
|
|
156
|
+
<svg
|
|
157
|
+
width="16"
|
|
158
|
+
height="16"
|
|
159
|
+
viewBox="0 0 24 24"
|
|
160
|
+
fill="none"
|
|
161
|
+
stroke="currentColor"
|
|
162
|
+
strokeWidth="2"
|
|
163
|
+
strokeLinecap="round"
|
|
164
|
+
strokeLinejoin="round"
|
|
165
|
+
>
|
|
166
|
+
<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" />
|
|
167
|
+
</svg>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const CloseIcon = (
|
|
171
|
+
<svg
|
|
172
|
+
width="12"
|
|
173
|
+
height="12"
|
|
174
|
+
viewBox="0 0 24 24"
|
|
175
|
+
fill="none"
|
|
176
|
+
stroke="currentColor"
|
|
177
|
+
strokeWidth="2"
|
|
178
|
+
strokeLinecap="round"
|
|
179
|
+
strokeLinejoin="round"
|
|
180
|
+
>
|
|
181
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
182
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
183
|
+
</svg>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const SparkleIcon = (
|
|
187
|
+
<svg
|
|
188
|
+
width="16"
|
|
189
|
+
height="16"
|
|
190
|
+
viewBox="0 0 24 24"
|
|
191
|
+
fill="none"
|
|
192
|
+
stroke="currentColor"
|
|
193
|
+
strokeWidth="2"
|
|
194
|
+
>
|
|
195
|
+
<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" />
|
|
196
|
+
</svg>
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const CheckIcon = (
|
|
200
|
+
<svg
|
|
201
|
+
width="14"
|
|
202
|
+
height="14"
|
|
203
|
+
viewBox="0 0 24 24"
|
|
204
|
+
fill="none"
|
|
205
|
+
stroke="currentColor"
|
|
206
|
+
strokeWidth="2"
|
|
207
|
+
strokeLinecap="round"
|
|
208
|
+
strokeLinejoin="round"
|
|
209
|
+
>
|
|
210
|
+
<polyline points="20 6 9 17 4 12" />
|
|
211
|
+
</svg>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const RefreshCwIcon = (
|
|
215
|
+
<svg
|
|
216
|
+
width="14"
|
|
217
|
+
height="14"
|
|
218
|
+
viewBox="0 0 24 24"
|
|
219
|
+
fill="none"
|
|
220
|
+
stroke="currentColor"
|
|
221
|
+
strokeWidth="2"
|
|
222
|
+
strokeLinecap="round"
|
|
223
|
+
strokeLinejoin="round"
|
|
224
|
+
>
|
|
225
|
+
<polyline points="23 4 23 10 17 10" />
|
|
226
|
+
<polyline points="1 20 1 14 7 14" />
|
|
227
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
|
228
|
+
</svg>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const SpinnerIcon = (
|
|
232
|
+
<svg
|
|
233
|
+
className="animate-spin"
|
|
234
|
+
width="14"
|
|
235
|
+
height="14"
|
|
236
|
+
viewBox="0 0 24 24"
|
|
237
|
+
fill="none"
|
|
238
|
+
stroke="currentColor"
|
|
239
|
+
strokeWidth="2"
|
|
240
|
+
>
|
|
241
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
242
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
|
243
|
+
</svg>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// ========== Status badge ==========
|
|
247
|
+
|
|
248
|
+
function StatusBadge({ status }: { status: BatchI2VRowStatus }) {
|
|
249
|
+
switch (status) {
|
|
250
|
+
case "generating":
|
|
251
|
+
return (
|
|
252
|
+
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-ink-600">
|
|
253
|
+
{SpinnerIcon}
|
|
254
|
+
Generating
|
|
255
|
+
</span>
|
|
256
|
+
);
|
|
257
|
+
case "done":
|
|
258
|
+
return (
|
|
259
|
+
<span className="inline-flex items-center gap-1 text-xs font-medium text-emerald-600">
|
|
260
|
+
{CheckIcon}
|
|
261
|
+
Done
|
|
262
|
+
</span>
|
|
263
|
+
);
|
|
264
|
+
case "error":
|
|
265
|
+
return <span className="text-xs font-medium text-red-600">Error</span>;
|
|
266
|
+
default:
|
|
267
|
+
return <span className="text-xs font-medium text-ink-500">—</span>;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ========== Duration cell (text input) ==========
|
|
272
|
+
|
|
273
|
+
function DurationCell({
|
|
274
|
+
value,
|
|
275
|
+
shared,
|
|
276
|
+
disabled,
|
|
277
|
+
onCommit,
|
|
278
|
+
}: {
|
|
279
|
+
value: number | null;
|
|
280
|
+
shared: number;
|
|
281
|
+
disabled: boolean;
|
|
282
|
+
onCommit: (v: number | null) => void;
|
|
283
|
+
}) {
|
|
284
|
+
const [text, setText] = useState(value == null ? "" : String(value));
|
|
285
|
+
|
|
286
|
+
// Sync local text when the row's value changes externally (e.g. CSV re-parse).
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
setText(value == null ? "" : String(value));
|
|
289
|
+
}, [value]);
|
|
290
|
+
|
|
291
|
+
const commit = () => {
|
|
292
|
+
const t = text.trim();
|
|
293
|
+
if (t === "") {
|
|
294
|
+
onCommit(null);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const n = Number(t);
|
|
298
|
+
if (Number.isFinite(n) && n > 0) {
|
|
299
|
+
onCommit(n);
|
|
300
|
+
} else {
|
|
301
|
+
setText(value == null ? "" : String(value));
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<input
|
|
307
|
+
type="text"
|
|
308
|
+
value={text}
|
|
309
|
+
onChange={(e) => setText(e.target.value)}
|
|
310
|
+
onBlur={commit}
|
|
311
|
+
onKeyDown={(e) => {
|
|
312
|
+
if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur();
|
|
313
|
+
}}
|
|
314
|
+
placeholder={`Default (${shared}s)`}
|
|
315
|
+
disabled={disabled}
|
|
316
|
+
title="Override video duration in seconds (empty = shared default)"
|
|
317
|
+
className="w-full px-2 py-1.5 bg-white border border-ink-200 rounded-xl text-ink-800 text-xs placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-1 focus:ring-tiffany-500/30 transition-all disabled:opacity-50"
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ========== Component ==========
|
|
323
|
+
|
|
324
|
+
export default function BatchImageToVideoTab({ projectId }: Props) {
|
|
325
|
+
const store = useBatchImageToVideoStore();
|
|
326
|
+
const { openFolder } = useProjectStore();
|
|
327
|
+
|
|
328
|
+
const logRef = useRef<HTMLDivElement | null>(null);
|
|
329
|
+
const csvInputRef = useRef<HTMLInputElement>(null);
|
|
330
|
+
const [csvFilename, setCsvFilename] = useState<string | null>(null);
|
|
331
|
+
const [csvText, setCsvText] = useState("");
|
|
332
|
+
const [preview, setPreview] = useState<{
|
|
333
|
+
url: string;
|
|
334
|
+
filename: string;
|
|
335
|
+
type: "image" | "video";
|
|
336
|
+
} | null>(null);
|
|
337
|
+
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
store.hydrate(projectId);
|
|
340
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
341
|
+
}, [projectId]);
|
|
342
|
+
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (logRef.current) {
|
|
345
|
+
logRef.current.scrollTop = logRef.current.scrollHeight;
|
|
346
|
+
}
|
|
347
|
+
}, [store.logs]);
|
|
348
|
+
|
|
349
|
+
// Close the preview modal on Escape.
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
if (!preview) return;
|
|
352
|
+
const onKey = (e: KeyboardEvent) => {
|
|
353
|
+
if (e.key === "Escape") setPreview(null);
|
|
354
|
+
};
|
|
355
|
+
window.addEventListener("keydown", onKey);
|
|
356
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
357
|
+
}, [preview]);
|
|
358
|
+
|
|
359
|
+
const handleCsvSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
360
|
+
const file = e.target.files?.[0];
|
|
361
|
+
if (!file) return;
|
|
362
|
+
|
|
363
|
+
const reader = new FileReader();
|
|
364
|
+
reader.onload = () => {
|
|
365
|
+
const base64 = reader.result as string;
|
|
366
|
+
store.uploadCsv(base64, file.name);
|
|
367
|
+
setCsvFilename(file.name);
|
|
368
|
+
};
|
|
369
|
+
reader.readAsDataURL(file);
|
|
370
|
+
e.target.value = "";
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const handlePasteCsv = () => {
|
|
374
|
+
if (!csvText.trim()) return;
|
|
375
|
+
store.parseCsvText(csvText);
|
|
376
|
+
setCsvFilename(null);
|
|
377
|
+
setCsvText("");
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const imageCount = store.rows.filter((r) => r.t2iPrompt.trim()).length;
|
|
381
|
+
const videoCount = store.rows.filter(
|
|
382
|
+
(r) => r.imagePath && r.i2vPrompt.trim(),
|
|
383
|
+
).length;
|
|
384
|
+
const bothCount = store.rows.filter(
|
|
385
|
+
(r) => r.t2iPrompt.trim() && r.i2vPrompt.trim(),
|
|
386
|
+
).length;
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div className="flex flex-col gap-7">
|
|
390
|
+
<div className="flex items-center gap-2">
|
|
391
|
+
{TableIcon}
|
|
392
|
+
<h2 className="text-base font-semibold text-ink-900">
|
|
393
|
+
Batch Image to Video
|
|
394
|
+
</h2>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{/* CSV upload */}
|
|
398
|
+
<div className="border border-ink-200 rounded-2xl p-5 flex flex-col gap-3">
|
|
399
|
+
<div className="flex items-center gap-2">
|
|
400
|
+
{FileIcon}
|
|
401
|
+
<span className="text-sm font-semibold text-ink-900">
|
|
402
|
+
Import Scenes (CSV)
|
|
403
|
+
</span>
|
|
404
|
+
</div>
|
|
405
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
406
|
+
<input
|
|
407
|
+
ref={csvInputRef}
|
|
408
|
+
type="file"
|
|
409
|
+
accept=".csv"
|
|
410
|
+
onChange={handleCsvSelect}
|
|
411
|
+
disabled={store.running}
|
|
412
|
+
className="hidden"
|
|
413
|
+
/>
|
|
414
|
+
<button
|
|
415
|
+
onClick={() => csvInputRef.current?.click()}
|
|
416
|
+
disabled={store.running}
|
|
417
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl border border-ink-200 bg-white text-ink-600 text-xs font-medium cursor-pointer hover:border-ink-300 hover:bg-ink-100 transition-colors disabled:opacity-50"
|
|
418
|
+
>
|
|
419
|
+
{UploadIcon}
|
|
420
|
+
Upload CSV
|
|
421
|
+
</button>
|
|
422
|
+
{csvFilename && (
|
|
423
|
+
<span className="text-[11px] text-ink-600 truncate max-w-[240px]">
|
|
424
|
+
{csvFilename}
|
|
425
|
+
</span>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div className="flex items-start gap-2">
|
|
430
|
+
<textarea
|
|
431
|
+
value={csvText}
|
|
432
|
+
onChange={(e) => setCsvText(e.target.value)}
|
|
433
|
+
placeholder={`Paste CSV here, e.g.\nt2i,i2v,duration\nA student sleeping in class...,Slow pan across the classroom...,5`}
|
|
434
|
+
rows={4}
|
|
435
|
+
disabled={store.running}
|
|
436
|
+
className="flex-1 px-2.5 py-1.5 bg-white border border-ink-200 rounded-xl text-ink-800 text-xs placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-1 focus:ring-tiffany-500/30 transition-all resize-y disabled:opacity-50 font-mono"
|
|
437
|
+
/>
|
|
438
|
+
<button
|
|
439
|
+
onClick={handlePasteCsv}
|
|
440
|
+
disabled={store.running || !csvText.trim()}
|
|
441
|
+
className="shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl border border-ink-200 bg-white text-ink-600 text-xs font-medium cursor-pointer hover:border-ink-300 hover:bg-ink-100 transition-colors disabled:opacity-50"
|
|
442
|
+
>
|
|
443
|
+
{ClipboardIcon}
|
|
444
|
+
Parse CSV
|
|
445
|
+
</button>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<p className="text-xs text-tiffany-600 italic">
|
|
449
|
+
CSV must contain columns <code className="text-[11px] bg-ink-100 px-1 rounded">t2i</code>{" "}
|
|
450
|
+
(text-to-image prompt) and{" "}
|
|
451
|
+
<code className="text-[11px] bg-ink-100 px-1 rounded">i2v</code>{" "}
|
|
452
|
+
(image-to-video prompt). An optional{" "}
|
|
453
|
+
<code className="text-[11px] bg-ink-100 px-1 rounded">duration</code>{" "}
|
|
454
|
+
column (seconds) overrides the shared duration for that row.
|
|
455
|
+
</p>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
{/* Shared settings */}
|
|
459
|
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
460
|
+
{/* Duration */}
|
|
461
|
+
<div>
|
|
462
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
463
|
+
Duration (seconds)
|
|
464
|
+
</label>
|
|
465
|
+
<div className="flex flex-wrap gap-1.5">
|
|
466
|
+
{[0.5, 3, 5, 7.5, 10, 15, 20].map((d) => (
|
|
467
|
+
<button
|
|
468
|
+
key={d}
|
|
469
|
+
onClick={() => store.setDuration(d)}
|
|
470
|
+
disabled={store.running}
|
|
471
|
+
className={`px-3 py-1 text-xs font-medium rounded-xl border transition-all ${
|
|
472
|
+
store.duration === d
|
|
473
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
474
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
475
|
+
} disabled:opacity-50`}
|
|
476
|
+
>
|
|
477
|
+
{d}s
|
|
478
|
+
</button>
|
|
479
|
+
))}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{/* Aspect ratio */}
|
|
484
|
+
<div>
|
|
485
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
486
|
+
Aspect Ratio
|
|
487
|
+
</label>
|
|
488
|
+
<div className="flex flex-wrap gap-1.5">
|
|
489
|
+
{(["1:1", "16:9", "9:16", "4:3", "3:4"] as const).map((ratio) => (
|
|
490
|
+
<button
|
|
491
|
+
key={ratio}
|
|
492
|
+
onClick={() => store.setAspectRatio(ratio)}
|
|
493
|
+
disabled={store.running}
|
|
494
|
+
className={`px-3 py-1 text-xs font-medium rounded-xl border transition-all ${
|
|
495
|
+
store.aspectRatio === ratio
|
|
496
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
497
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
498
|
+
} disabled:opacity-50`}
|
|
499
|
+
>
|
|
500
|
+
{ratio}
|
|
501
|
+
</button>
|
|
502
|
+
))}
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
{/* Resolution */}
|
|
507
|
+
<div>
|
|
508
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
509
|
+
Resolution
|
|
510
|
+
</label>
|
|
511
|
+
<div className="flex flex-wrap gap-1.5">
|
|
512
|
+
{(["320p", "480p", "576p", "640p", "720p", "1080p"] as const).map(
|
|
513
|
+
(res: any) => (
|
|
514
|
+
<button
|
|
515
|
+
key={res}
|
|
516
|
+
onClick={() => store.setResolution(res)}
|
|
517
|
+
disabled={store.running}
|
|
518
|
+
className={`px-3 py-1 text-xs font-medium rounded-xl border transition-all ${
|
|
519
|
+
store.resolution === res
|
|
520
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
521
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
522
|
+
} disabled:opacity-50`}
|
|
523
|
+
>
|
|
524
|
+
{res}
|
|
525
|
+
</button>
|
|
526
|
+
),
|
|
527
|
+
)}
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
{/* Video mode */}
|
|
532
|
+
<div>
|
|
533
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
534
|
+
Mode
|
|
535
|
+
</label>
|
|
536
|
+
<div className="flex flex-wrap gap-1.5">
|
|
537
|
+
{(
|
|
538
|
+
[
|
|
539
|
+
{ label: "Low", value: "distilled" },
|
|
540
|
+
{ label: "Standard", value: "one-stage" },
|
|
541
|
+
{ label: "High", value: "two-stage" },
|
|
542
|
+
] as const
|
|
543
|
+
).map((q) => (
|
|
544
|
+
<button
|
|
545
|
+
key={q.value}
|
|
546
|
+
onClick={() => store.setMode(q.value)}
|
|
547
|
+
disabled={store.running}
|
|
548
|
+
className={`px-3 py-1 text-xs font-medium rounded-xl border transition-all ${
|
|
549
|
+
store.mode === q.value
|
|
550
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
551
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
552
|
+
} disabled:opacity-50`}
|
|
553
|
+
>
|
|
554
|
+
{q.label}
|
|
555
|
+
</button>
|
|
556
|
+
))}
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
{/* Table */}
|
|
562
|
+
<div>
|
|
563
|
+
<div className="flex items-center justify-between mb-2">
|
|
564
|
+
<label className="text-xs font-semibold text-ink-700 uppercase tracking-wider">
|
|
565
|
+
Scenes
|
|
566
|
+
</label>
|
|
567
|
+
<div className="flex items-center gap-1.5">
|
|
568
|
+
<button
|
|
569
|
+
onClick={store.clear}
|
|
570
|
+
disabled={store.running}
|
|
571
|
+
title="Clear all rows and settings"
|
|
572
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl border transition-all bg-white border-ink-200 text-red-600 hover:border-red-200 hover:bg-red-50 disabled:opacity-50"
|
|
573
|
+
>
|
|
574
|
+
{CloseIcon}
|
|
575
|
+
Clear
|
|
576
|
+
</button>
|
|
577
|
+
<button
|
|
578
|
+
onClick={store.addRow}
|
|
579
|
+
disabled={store.running}
|
|
580
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300 disabled:opacity-50"
|
|
581
|
+
>
|
|
582
|
+
{PlusIcon}
|
|
583
|
+
Add Row
|
|
584
|
+
</button>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div className="border border-ink-200 rounded-2xl overflow-hidden">
|
|
589
|
+
<div className="overflow-auto max-h-[480px]">
|
|
590
|
+
<table className="w-full text-xs">
|
|
591
|
+
<thead>
|
|
592
|
+
<tr className="bg-ink-50 border-b border-ink-200">
|
|
593
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 w-10">
|
|
594
|
+
#
|
|
595
|
+
</th>
|
|
596
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 min-w-[220px]">
|
|
597
|
+
Text-to-Image Prompt
|
|
598
|
+
</th>
|
|
599
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 min-w-[220px]">
|
|
600
|
+
Image-to-Video Prompt
|
|
601
|
+
</th>
|
|
602
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 w-28">
|
|
603
|
+
Duration
|
|
604
|
+
</th>
|
|
605
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 w-32">
|
|
606
|
+
Image
|
|
607
|
+
</th>
|
|
608
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 w-40">
|
|
609
|
+
Video
|
|
610
|
+
</th>
|
|
611
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 w-32">
|
|
612
|
+
Status
|
|
613
|
+
</th>
|
|
614
|
+
<th className="sticky top-0 bg-ink-50 px-3 py-2 text-left font-semibold text-ink-700 w-36">
|
|
615
|
+
Actions
|
|
616
|
+
</th>
|
|
617
|
+
</tr>
|
|
618
|
+
</thead>
|
|
619
|
+
<tbody>
|
|
620
|
+
{store.rows.map((row, idx) => (
|
|
621
|
+
<tr
|
|
622
|
+
key={row.id}
|
|
623
|
+
className="border-b border-ink-200 last:border-b-0 align-top transition-colors hover:bg-ink-100/40"
|
|
624
|
+
>
|
|
625
|
+
<td className="px-3 py-2 text-ink-600/60 font-medium tabular-nums">
|
|
626
|
+
{idx + 1}
|
|
627
|
+
</td>
|
|
628
|
+
|
|
629
|
+
{/* Text-to-image prompt */}
|
|
630
|
+
<td className="px-3 py-2">
|
|
631
|
+
<textarea
|
|
632
|
+
value={row.t2iPrompt}
|
|
633
|
+
onChange={(e) =>
|
|
634
|
+
store.updateT2IPrompt(row.id, e.target.value)
|
|
635
|
+
}
|
|
636
|
+
placeholder="Describe the starting image..."
|
|
637
|
+
rows={3}
|
|
638
|
+
disabled={store.running}
|
|
639
|
+
className="w-full px-2.5 py-1.5 bg-transparent border border-ink-200 rounded-xl text-ink-800 text-xs placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-1 focus:ring-tiffany-500/30 transition-all resize-none disabled:opacity-50"
|
|
640
|
+
/>
|
|
641
|
+
</td>
|
|
642
|
+
|
|
643
|
+
{/* Image-to-video prompt */}
|
|
644
|
+
<td className="px-3 py-2">
|
|
645
|
+
<textarea
|
|
646
|
+
value={row.i2vPrompt}
|
|
647
|
+
onChange={(e) =>
|
|
648
|
+
store.updateI2VPrompt(row.id, e.target.value)
|
|
649
|
+
}
|
|
650
|
+
placeholder="Describe the motion..."
|
|
651
|
+
rows={3}
|
|
652
|
+
disabled={store.running}
|
|
653
|
+
className="w-full px-2.5 py-1.5 bg-transparent border border-ink-200 rounded-xl text-ink-800 text-xs placeholder-ink-500/40 focus:outline-none focus:border-tiffany-500 focus:ring-1 focus:ring-tiffany-500/30 transition-all resize-none disabled:opacity-50"
|
|
654
|
+
/>
|
|
655
|
+
</td>
|
|
656
|
+
|
|
657
|
+
{/* Duration override */}
|
|
658
|
+
<td className="px-3 py-2">
|
|
659
|
+
<DurationCell
|
|
660
|
+
value={row.duration}
|
|
661
|
+
shared={store.duration}
|
|
662
|
+
disabled={store.running}
|
|
663
|
+
onCommit={(v) => store.updateRowDuration(row.id, v)}
|
|
664
|
+
/>
|
|
665
|
+
</td>
|
|
666
|
+
|
|
667
|
+
{/* Generated image */}
|
|
668
|
+
<td className="px-3 py-2">
|
|
669
|
+
{row.imageUrl ? (
|
|
670
|
+
<img
|
|
671
|
+
src={row.imageUrl}
|
|
672
|
+
alt={row.imageFilename || "Generated image"}
|
|
673
|
+
onClick={() =>
|
|
674
|
+
setPreview({
|
|
675
|
+
url: row.imageUrl!,
|
|
676
|
+
filename: row.imageFilename || "Image",
|
|
677
|
+
type: "image",
|
|
678
|
+
})
|
|
679
|
+
}
|
|
680
|
+
className="w-20 h-20 object-cover rounded-xl border border-ink-200 cursor-zoom-in hover:opacity-80 transition-opacity"
|
|
681
|
+
/>
|
|
682
|
+
) : (
|
|
683
|
+
<span className="text-[10px] text-ink-500 italic">
|
|
684
|
+
—
|
|
685
|
+
</span>
|
|
686
|
+
)}
|
|
687
|
+
</td>
|
|
688
|
+
|
|
689
|
+
{/* Generated video */}
|
|
690
|
+
<td className="px-3 py-2">
|
|
691
|
+
{row.videoResult ? (
|
|
692
|
+
<div className="relative inline-block">
|
|
693
|
+
<video
|
|
694
|
+
src={row.videoResult}
|
|
695
|
+
controls
|
|
696
|
+
className="w-full max-w-[160px] h-auto rounded-xl bg-black"
|
|
697
|
+
/>
|
|
698
|
+
<button
|
|
699
|
+
onClick={() =>
|
|
700
|
+
setPreview({
|
|
701
|
+
url: row.videoResult!,
|
|
702
|
+
filename: "Video",
|
|
703
|
+
type: "video",
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
className="absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded-full bg-black/60 text-white hover:bg-black/80 transition-colors"
|
|
707
|
+
title="Preview"
|
|
708
|
+
>
|
|
709
|
+
{ExpandIcon}
|
|
710
|
+
</button>
|
|
711
|
+
</div>
|
|
712
|
+
) : (
|
|
713
|
+
<span className="text-[10px] text-ink-500 italic">
|
|
714
|
+
—
|
|
715
|
+
</span>
|
|
716
|
+
)}
|
|
717
|
+
</td>
|
|
718
|
+
|
|
719
|
+
{/* Status */}
|
|
720
|
+
<td className="px-3 py-2">
|
|
721
|
+
<div className="flex flex-col gap-1">
|
|
722
|
+
<div className="flex items-center gap-1">
|
|
723
|
+
<span className="text-[10px] text-tiffany-600 w-10">
|
|
724
|
+
Img:
|
|
725
|
+
</span>
|
|
726
|
+
<StatusBadge status={row.imageStatus} />
|
|
727
|
+
</div>
|
|
728
|
+
<div className="flex items-center gap-1">
|
|
729
|
+
<span className="text-[10px] text-tiffany-600 w-10">
|
|
730
|
+
Vid:
|
|
731
|
+
</span>
|
|
732
|
+
<StatusBadge status={row.videoStatus} />
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
{row.error && (
|
|
736
|
+
<p className="text-[10px] text-red-600 mt-1 break-words max-w-[120px]">
|
|
737
|
+
{row.error}
|
|
738
|
+
</p>
|
|
739
|
+
)}
|
|
740
|
+
</td>
|
|
741
|
+
|
|
742
|
+
{/* Actions */}
|
|
743
|
+
<td className="px-3 py-2">
|
|
744
|
+
<div className="flex flex-wrap gap-1">
|
|
745
|
+
<button
|
|
746
|
+
onClick={() =>
|
|
747
|
+
store.regenerateImage(projectId, row.id)
|
|
748
|
+
}
|
|
749
|
+
disabled={store.running || !row.t2iPrompt.trim()}
|
|
750
|
+
title="Regenerate image"
|
|
751
|
+
className="flex items-center justify-center w-7 h-7 rounded-xl border border-ink-200 text-ink-600 hover:bg-ink-100 hover:text-ink-800 hover:border-ink-300 transition-colors disabled:opacity-40"
|
|
752
|
+
>
|
|
753
|
+
{ImageIcon}
|
|
754
|
+
</button>
|
|
755
|
+
<button
|
|
756
|
+
onClick={() =>
|
|
757
|
+
store.regenerateVideo(projectId, row.id)
|
|
758
|
+
}
|
|
759
|
+
disabled={
|
|
760
|
+
store.running ||
|
|
761
|
+
!row.imagePath ||
|
|
762
|
+
!row.i2vPrompt.trim()
|
|
763
|
+
}
|
|
764
|
+
title="Regenerate video"
|
|
765
|
+
className="flex items-center justify-center w-7 h-7 rounded-xl border border-ink-200 text-ink-600 hover:bg-ink-100 hover:text-ink-800 hover:border-ink-300 transition-colors disabled:opacity-40"
|
|
766
|
+
>
|
|
767
|
+
{FilmIcon}
|
|
768
|
+
</button>
|
|
769
|
+
<button
|
|
770
|
+
onClick={() => store.regenerateBoth(projectId, row.id)}
|
|
771
|
+
disabled={
|
|
772
|
+
store.running ||
|
|
773
|
+
!row.t2iPrompt.trim() ||
|
|
774
|
+
!row.i2vPrompt.trim()
|
|
775
|
+
}
|
|
776
|
+
title="Regenerate image + video"
|
|
777
|
+
className="flex items-center justify-center w-7 h-7 rounded-xl border border-ink-200 text-ink-600 hover:bg-ink-100 hover:text-ink-800 hover:border-ink-300 transition-colors disabled:opacity-40"
|
|
778
|
+
>
|
|
779
|
+
{RefreshCwIcon}
|
|
780
|
+
</button>
|
|
781
|
+
<button
|
|
782
|
+
onClick={() => store.removeRow(row.id)}
|
|
783
|
+
disabled={store.running}
|
|
784
|
+
title="Remove row"
|
|
785
|
+
className="flex items-center justify-center w-7 h-7 rounded-xl border border-ink-200 text-ink-600 hover:bg-red-50 hover:text-red-600 hover:border-red-200 transition-colors disabled:opacity-40"
|
|
786
|
+
>
|
|
787
|
+
{CloseIcon}
|
|
788
|
+
</button>
|
|
789
|
+
</div>
|
|
790
|
+
</td>
|
|
791
|
+
</tr>
|
|
792
|
+
))}
|
|
793
|
+
</tbody>
|
|
794
|
+
</table>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
|
|
798
|
+
{store.rows.length === 0 && (
|
|
799
|
+
<p className="text-xs text-ink-500 italic py-6 text-center border border-dashed border-ink-200 rounded-2xl mt-2">
|
|
800
|
+
No scenes yet. Click "Add Row" or upload a CSV.
|
|
801
|
+
</p>
|
|
802
|
+
)}
|
|
803
|
+
</div>
|
|
804
|
+
|
|
805
|
+
{/* Open output folder */}
|
|
806
|
+
<button
|
|
807
|
+
onClick={() => openFolder(projectId, "output")}
|
|
808
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-ink-50 hover:bg-ink-200 text-ink-700 text-sm font-medium rounded-2xl border border-ink-200 transition-colors"
|
|
809
|
+
>
|
|
810
|
+
{FolderIcon}
|
|
811
|
+
Open Output Folder
|
|
812
|
+
</button>
|
|
813
|
+
|
|
814
|
+
{/* Progress */}
|
|
815
|
+
{store.progress && (
|
|
816
|
+
<div className="flex items-center gap-3 p-4 bg-ink-50 border border-ink-200 rounded-2xl">
|
|
817
|
+
<span className="text-xs font-medium text-ink-700">
|
|
818
|
+
Batch Progress
|
|
819
|
+
</span>
|
|
820
|
+
<div className="flex-1 h-2 bg-ink-200 rounded-full overflow-hidden">
|
|
821
|
+
<div
|
|
822
|
+
className="h-full bg-tiffany-500 rounded-full transition-all duration-300"
|
|
823
|
+
style={{
|
|
824
|
+
width: `${(store.progress.current / store.progress.total) * 100}%`,
|
|
825
|
+
}}
|
|
826
|
+
/>
|
|
827
|
+
</div>
|
|
828
|
+
<span className="text-xs font-semibold text-ink-700 tabular-nums">
|
|
829
|
+
{store.progress.current}/{store.progress.total}
|
|
830
|
+
</span>
|
|
831
|
+
</div>
|
|
832
|
+
)}
|
|
833
|
+
|
|
834
|
+
{/* Logs */}
|
|
835
|
+
{store.logs.length > 0 && (
|
|
836
|
+
<div
|
|
837
|
+
ref={logRef}
|
|
838
|
+
className="p-5 bg-ink-50 border border-ink-200 rounded-2xl max-h-40 overflow-y-auto"
|
|
839
|
+
>
|
|
840
|
+
<p className="text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
841
|
+
Progress Logs
|
|
842
|
+
</p>
|
|
843
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
844
|
+
{store.logs.join("\n")}
|
|
845
|
+
</pre>
|
|
846
|
+
</div>
|
|
847
|
+
)}
|
|
848
|
+
|
|
849
|
+
{/* Generate buttons */}
|
|
850
|
+
{store.running ? (
|
|
851
|
+
<button
|
|
852
|
+
onClick={store.cancel}
|
|
853
|
+
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"
|
|
854
|
+
>
|
|
855
|
+
Cancel Batch
|
|
856
|
+
</button>
|
|
857
|
+
) : (
|
|
858
|
+
<div className="flex flex-col gap-2">
|
|
859
|
+
<button
|
|
860
|
+
onClick={() => store.generateAll(projectId)}
|
|
861
|
+
disabled={bothCount === 0}
|
|
862
|
+
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-glow hover:shadow-glow disabled:shadow-none"
|
|
863
|
+
>
|
|
864
|
+
{SparkleIcon}
|
|
865
|
+
Generate All Images & Videos ({bothCount})
|
|
866
|
+
</button>
|
|
867
|
+
<div className="grid grid-cols-2 gap-2">
|
|
868
|
+
<button
|
|
869
|
+
onClick={() => store.generateAllImages(projectId)}
|
|
870
|
+
disabled={imageCount === 0}
|
|
871
|
+
className="flex items-center justify-center gap-2 px-4 py-2.5 bg-white hover:bg-ink-100 disabled:bg-ink-50 disabled:text-ink-500 text-ink-700 text-sm font-medium rounded-2xl border border-ink-200 transition-colors disabled:border-ink-200"
|
|
872
|
+
>
|
|
873
|
+
{ImageIcon}
|
|
874
|
+
Generate All Images ({imageCount})
|
|
875
|
+
</button>
|
|
876
|
+
<button
|
|
877
|
+
onClick={() => store.generateAllVideos(projectId)}
|
|
878
|
+
disabled={videoCount === 0}
|
|
879
|
+
className="flex items-center justify-center gap-2 px-4 py-2.5 bg-white hover:bg-ink-100 disabled:bg-ink-50 disabled:text-ink-500 text-ink-700 text-sm font-medium rounded-2xl border border-ink-200 transition-colors disabled:border-ink-200"
|
|
880
|
+
>
|
|
881
|
+
{FilmIcon}
|
|
882
|
+
Generate All Videos ({videoCount})
|
|
883
|
+
</button>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
)}
|
|
887
|
+
|
|
888
|
+
{/* ===== Preview Modal ===== */}
|
|
889
|
+
{preview && (
|
|
890
|
+
<div
|
|
891
|
+
className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-8"
|
|
892
|
+
onClick={() => setPreview(null)}
|
|
893
|
+
>
|
|
894
|
+
<div
|
|
895
|
+
className="relative max-w-[90vw] max-h-[90vh] flex flex-col items-center gap-3"
|
|
896
|
+
onClick={(e) => e.stopPropagation()}
|
|
897
|
+
>
|
|
898
|
+
{preview.type === "image" ? (
|
|
899
|
+
<img
|
|
900
|
+
src={preview.url}
|
|
901
|
+
alt={preview.filename}
|
|
902
|
+
className="max-w-full max-h-[80vh] object-contain rounded-2xl shadow-2xl"
|
|
903
|
+
/>
|
|
904
|
+
) : (
|
|
905
|
+
<video
|
|
906
|
+
src={preview.url}
|
|
907
|
+
controls
|
|
908
|
+
autoPlay
|
|
909
|
+
className="max-w-full max-h-[80vh] rounded-2xl shadow-2xl bg-black"
|
|
910
|
+
/>
|
|
911
|
+
)}
|
|
912
|
+
<span className="text-xs text-white/70 truncate max-w-full">
|
|
913
|
+
{preview.filename}
|
|
914
|
+
</span>
|
|
915
|
+
<button
|
|
916
|
+
onClick={() => setPreview(null)}
|
|
917
|
+
className="absolute -top-3 -right-3 flex items-center justify-center w-9 h-9 bg-white text-ink-700 rounded-full shadow-lg hover:bg-ink-200 transition-colors"
|
|
918
|
+
title="Close (Esc)"
|
|
919
|
+
>
|
|
920
|
+
{CloseIcon}
|
|
921
|
+
</button>
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
)}
|
|
925
|
+
</div>
|
|
926
|
+
);
|
|
927
|
+
}
|