@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,574 @@
|
|
|
1
|
+
import { useEffect, useState } 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 TextToImageTab({ projectId }: Props) {
|
|
10
|
+
const store = useGenerationStore();
|
|
11
|
+
const { openFolder } = useProjectStore();
|
|
12
|
+
const [preview, setPreview] = useState<{
|
|
13
|
+
url: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
} | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
store.hydrateTextToImage(projectId);
|
|
19
|
+
store.checkTextToImageStatus();
|
|
20
|
+
store.fetchProjectImages(projectId);
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
}, [projectId]);
|
|
23
|
+
|
|
24
|
+
// Close the preview modal on Escape.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!preview) return;
|
|
27
|
+
const onKey = (e: KeyboardEvent) => {
|
|
28
|
+
if (e.key === "Escape") setPreview(null);
|
|
29
|
+
};
|
|
30
|
+
window.addEventListener("keydown", onKey);
|
|
31
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
32
|
+
}, [preview]);
|
|
33
|
+
|
|
34
|
+
const busy =
|
|
35
|
+
store.textToImage.generating ||
|
|
36
|
+
store.textToImage.installing ||
|
|
37
|
+
store.textToImage.downloading;
|
|
38
|
+
|
|
39
|
+
const generatedImages = store.projectImages.filter(
|
|
40
|
+
(img) => img.source === "generated",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const renderStatus = (
|
|
44
|
+
value: boolean | null,
|
|
45
|
+
okText: string,
|
|
46
|
+
missingText: string,
|
|
47
|
+
) => {
|
|
48
|
+
if (value === null) {
|
|
49
|
+
return (
|
|
50
|
+
<span className="inline-flex items-center gap-1.5 text-ink-500">
|
|
51
|
+
{SpinnerIcon}
|
|
52
|
+
Checking...
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (value) {
|
|
57
|
+
return (
|
|
58
|
+
<span className="inline-flex items-center gap-1.5 text-emerald-600">
|
|
59
|
+
{CheckCircleIcon}
|
|
60
|
+
{okText}
|
|
61
|
+
</span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<span className="inline-flex items-center gap-1.5 text-amber-600">
|
|
66
|
+
{AlertCircleIcon}
|
|
67
|
+
{missingText}
|
|
68
|
+
</span>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ========== SVG Icons ==========
|
|
73
|
+
|
|
74
|
+
const ImageIcon = (
|
|
75
|
+
<svg
|
|
76
|
+
width="18"
|
|
77
|
+
height="18"
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
fill="none"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
strokeWidth="2"
|
|
82
|
+
strokeLinecap="round"
|
|
83
|
+
strokeLinejoin="round"
|
|
84
|
+
>
|
|
85
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
86
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
87
|
+
<polyline points="21 15 16 10 5 21" />
|
|
88
|
+
</svg>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const InstallIcon = (
|
|
92
|
+
<svg
|
|
93
|
+
width="16"
|
|
94
|
+
height="16"
|
|
95
|
+
viewBox="0 0 24 24"
|
|
96
|
+
fill="none"
|
|
97
|
+
stroke="currentColor"
|
|
98
|
+
strokeWidth="2"
|
|
99
|
+
strokeLinecap="round"
|
|
100
|
+
strokeLinejoin="round"
|
|
101
|
+
>
|
|
102
|
+
<polyline points="16 18 22 12 16 6" />
|
|
103
|
+
<polyline points="8 6 2 12 8 18" />
|
|
104
|
+
</svg>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const DownloadIcon = (
|
|
108
|
+
<svg
|
|
109
|
+
width="16"
|
|
110
|
+
height="16"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
fill="none"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
strokeWidth="2"
|
|
115
|
+
strokeLinecap="round"
|
|
116
|
+
strokeLinejoin="round"
|
|
117
|
+
>
|
|
118
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
119
|
+
<polyline points="7 10 12 15 17 10" />
|
|
120
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
121
|
+
</svg>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const SparkleIcon = (
|
|
125
|
+
<svg
|
|
126
|
+
width="16"
|
|
127
|
+
height="16"
|
|
128
|
+
viewBox="0 0 24 24"
|
|
129
|
+
fill="none"
|
|
130
|
+
stroke="currentColor"
|
|
131
|
+
strokeWidth="2"
|
|
132
|
+
>
|
|
133
|
+
<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" />
|
|
134
|
+
</svg>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const SpinnerIcon = (
|
|
138
|
+
<svg
|
|
139
|
+
className="animate-spin text-tiffany-600"
|
|
140
|
+
width="16"
|
|
141
|
+
height="16"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
fill="none"
|
|
144
|
+
stroke="currentColor"
|
|
145
|
+
strokeWidth="2"
|
|
146
|
+
>
|
|
147
|
+
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
|
148
|
+
<path d="M12 2a10 10 0 0 1 10 10" strokeOpacity="0.75" />
|
|
149
|
+
</svg>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const CheckCircleIcon = (
|
|
153
|
+
<svg
|
|
154
|
+
width="14"
|
|
155
|
+
height="14"
|
|
156
|
+
viewBox="0 0 24 24"
|
|
157
|
+
fill="none"
|
|
158
|
+
stroke="currentColor"
|
|
159
|
+
strokeWidth="2"
|
|
160
|
+
strokeLinecap="round"
|
|
161
|
+
strokeLinejoin="round"
|
|
162
|
+
>
|
|
163
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
164
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
165
|
+
</svg>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const AlertCircleIcon = (
|
|
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
|
+
<circle cx="12" cy="12" r="10" />
|
|
180
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
181
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
182
|
+
</svg>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const CloseIcon = (
|
|
186
|
+
<svg
|
|
187
|
+
width="18"
|
|
188
|
+
height="18"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
fill="none"
|
|
191
|
+
stroke="currentColor"
|
|
192
|
+
strokeWidth="2"
|
|
193
|
+
strokeLinecap="round"
|
|
194
|
+
strokeLinejoin="round"
|
|
195
|
+
>
|
|
196
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
197
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
198
|
+
</svg>
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const FolderIcon = (
|
|
202
|
+
<svg
|
|
203
|
+
width="16"
|
|
204
|
+
height="16"
|
|
205
|
+
viewBox="0 0 24 24"
|
|
206
|
+
fill="none"
|
|
207
|
+
stroke="currentColor"
|
|
208
|
+
strokeWidth="2"
|
|
209
|
+
strokeLinecap="round"
|
|
210
|
+
strokeLinejoin="round"
|
|
211
|
+
>
|
|
212
|
+
<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" />
|
|
213
|
+
</svg>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="flex flex-col gap-7">
|
|
218
|
+
<div className="flex items-center gap-2">
|
|
219
|
+
<span className="text-tiffany-600">{ImageIcon}</span>
|
|
220
|
+
<h2 className="text-base font-semibold text-ink-900">
|
|
221
|
+
Text-to-Image Generation
|
|
222
|
+
</h2>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* ===== Presets ===== */}
|
|
226
|
+
<div>
|
|
227
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
228
|
+
Presets
|
|
229
|
+
</label>
|
|
230
|
+
<div className="flex flex-wrap gap-2">
|
|
231
|
+
<button
|
|
232
|
+
onClick={() => store.applyTextToImagePreset("prototype")}
|
|
233
|
+
disabled={busy}
|
|
234
|
+
className="flex items-center gap-1.5 px-4 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"
|
|
235
|
+
>
|
|
236
|
+
Prototype
|
|
237
|
+
</button>
|
|
238
|
+
<button
|
|
239
|
+
onClick={() => store.applyTextToImagePreset("medium")}
|
|
240
|
+
disabled={busy}
|
|
241
|
+
className="flex items-center gap-1.5 px-4 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"
|
|
242
|
+
>
|
|
243
|
+
Medium
|
|
244
|
+
</button>
|
|
245
|
+
<button
|
|
246
|
+
onClick={() => store.applyTextToImagePreset("optimal")}
|
|
247
|
+
disabled={busy}
|
|
248
|
+
className="flex items-center gap-1.5 px-4 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"
|
|
249
|
+
>
|
|
250
|
+
Optimal
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
<p className="text-xs text-tiffany-600 mt-1.5">
|
|
254
|
+
Prototype: 1:1 · 320p · 4 steps · faster model. Medium: 1:1 · 720p · 7
|
|
255
|
+
steps · high quality model. Optimal: 1:1 · 1080p · 6 steps · high
|
|
256
|
+
quality model.
|
|
257
|
+
</p>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* ===== Setup: install + download model ===== */}
|
|
261
|
+
<div>
|
|
262
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
263
|
+
Model Setup
|
|
264
|
+
</label>
|
|
265
|
+
<div className="flex flex-wrap gap-2">
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => store.installTextToImage()}
|
|
268
|
+
disabled={store.textToImage.installing}
|
|
269
|
+
className="flex items-center gap-2 px-4 py-2 text-xs font-medium rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300 disabled:opacity-50"
|
|
270
|
+
>
|
|
271
|
+
{store.textToImage.installing ? SpinnerIcon : InstallIcon}
|
|
272
|
+
Install mlx-gen
|
|
273
|
+
</button>
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => store.downloadTextToImageModel()}
|
|
276
|
+
disabled={store.textToImage.downloading}
|
|
277
|
+
className="flex items-center gap-2 px-4 py-2 text-xs font-medium rounded-xl border transition-all bg-white border-ink-200 text-ink-600 hover:border-ink-300 disabled:opacity-50"
|
|
278
|
+
>
|
|
279
|
+
{store.textToImage.downloading ? SpinnerIcon : DownloadIcon}
|
|
280
|
+
Download z-image Model
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 mt-2 text-xs font-medium">
|
|
285
|
+
{renderStatus(
|
|
286
|
+
store.textToImage.mlxgenInstalled,
|
|
287
|
+
"mlx-gen installed",
|
|
288
|
+
"mlx-gen not installed",
|
|
289
|
+
)}
|
|
290
|
+
{renderStatus(
|
|
291
|
+
store.textToImage.zModelDownloaded,
|
|
292
|
+
"z-image model downloaded",
|
|
293
|
+
"z-image model not downloaded",
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{store.textToImage.installingLogs.length > 0 && (
|
|
298
|
+
<div className="mt-2 p-4 bg-ink-50 border border-ink-200 rounded-2xl max-h-32 overflow-y-auto">
|
|
299
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
300
|
+
{store.textToImage.installingLogs.join("")}
|
|
301
|
+
</pre>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
{store.textToImage.installingError && (
|
|
305
|
+
<div className="mt-2 p-4 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-xs">
|
|
306
|
+
{store.textToImage.installingError}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
{store.textToImage.downloadingLogs.length > 0 && (
|
|
311
|
+
<div className="mt-2 p-4 bg-ink-50 border border-ink-200 rounded-2xl max-h-32 overflow-y-auto">
|
|
312
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
313
|
+
{store.textToImage.downloadingLogs.join("")}
|
|
314
|
+
</pre>
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
{store.textToImage.downloadingError && (
|
|
318
|
+
<div className="mt-2 p-4 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-xs">
|
|
319
|
+
{store.textToImage.downloadingError}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{/* ===== Prompt ===== */}
|
|
325
|
+
<div>
|
|
326
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
327
|
+
Image Prompt
|
|
328
|
+
</label>
|
|
329
|
+
<textarea
|
|
330
|
+
value={store.textToImage.prompt}
|
|
331
|
+
onChange={(e) => store.setTextToImagePrompt(e.target.value)}
|
|
332
|
+
placeholder="Describe the image to generate, e.g. a little lamb standing in a sunny meadow."
|
|
333
|
+
rows={4}
|
|
334
|
+
disabled={busy}
|
|
335
|
+
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"
|
|
336
|
+
/>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* ===== Aspect Ratio ===== */}
|
|
340
|
+
<div>
|
|
341
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
342
|
+
Aspect Ratio
|
|
343
|
+
</label>
|
|
344
|
+
<div className="flex flex-wrap gap-2">
|
|
345
|
+
{(["1:1", "16:9", "9:16", "4:3", "3:4"] as const).map((ratio) => (
|
|
346
|
+
<button
|
|
347
|
+
key={ratio}
|
|
348
|
+
onClick={() => store.setTextToImageAspectRatio(ratio)}
|
|
349
|
+
disabled={busy}
|
|
350
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
351
|
+
store.textToImage.aspectRatio === ratio
|
|
352
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
353
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
354
|
+
} disabled:opacity-50`}
|
|
355
|
+
>
|
|
356
|
+
{ratio}
|
|
357
|
+
</button>
|
|
358
|
+
))}
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{/* ===== Resolution ===== */}
|
|
363
|
+
<div>
|
|
364
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
365
|
+
Resolution
|
|
366
|
+
</label>
|
|
367
|
+
<div className="flex flex-wrap gap-2">
|
|
368
|
+
{(
|
|
369
|
+
["320p", "480p", "512p", "640p", "720p", "1080p", "2048p"] as const
|
|
370
|
+
).map((res) => (
|
|
371
|
+
<button
|
|
372
|
+
key={res}
|
|
373
|
+
onClick={() => store.setTextToImageResolution(res)}
|
|
374
|
+
disabled={busy}
|
|
375
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
376
|
+
store.textToImage.resolution === res
|
|
377
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
378
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
379
|
+
} disabled:opacity-50`}
|
|
380
|
+
>
|
|
381
|
+
{res}
|
|
382
|
+
</button>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
{/* ===== Steps ===== */}
|
|
388
|
+
<div>
|
|
389
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
390
|
+
Steps
|
|
391
|
+
</label>
|
|
392
|
+
<div className="flex flex-wrap gap-2">
|
|
393
|
+
{[4, 5, 6, 7, 8, 9, 10, 11, 12].map((s) => (
|
|
394
|
+
<button
|
|
395
|
+
key={s}
|
|
396
|
+
onClick={() => store.setTextToImageSteps(s)}
|
|
397
|
+
disabled={busy}
|
|
398
|
+
className={`px-4 py-1.5 text-xs font-medium rounded-xl border transition-all ${
|
|
399
|
+
store.textToImage.steps === s
|
|
400
|
+
? "bg-ink-100 border-ink-300 text-ink-800"
|
|
401
|
+
: "bg-white border-ink-200 text-ink-600 hover:border-ink-300"
|
|
402
|
+
} disabled:opacity-50`}
|
|
403
|
+
>
|
|
404
|
+
{s} steps
|
|
405
|
+
</button>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{/* ===== Generate ===== */}
|
|
411
|
+
{store.textToImage.generating ? (
|
|
412
|
+
<div className="flex items-center gap-3">
|
|
413
|
+
<div className="flex items-center gap-2 flex-1 px-4 py-3 bg-ink-50 border border-ink-200 rounded-2xl">
|
|
414
|
+
{SpinnerIcon}
|
|
415
|
+
<span className="text-sm font-medium text-ink-700">
|
|
416
|
+
Generating...
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
<button
|
|
420
|
+
onClick={() => store.cancelGenerate()}
|
|
421
|
+
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"
|
|
422
|
+
>
|
|
423
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
424
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
425
|
+
</svg>
|
|
426
|
+
Stop
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
) : (
|
|
430
|
+
<button
|
|
431
|
+
onClick={() => store.generateTextToImage(projectId)}
|
|
432
|
+
disabled={busy || !store.textToImage.prompt.trim()}
|
|
433
|
+
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"
|
|
434
|
+
>
|
|
435
|
+
{SparkleIcon}
|
|
436
|
+
Generate Image
|
|
437
|
+
</button>
|
|
438
|
+
)}
|
|
439
|
+
|
|
440
|
+
{/* ===== Open output folder ===== */}
|
|
441
|
+
<button
|
|
442
|
+
onClick={() => openFolder(projectId, "output")}
|
|
443
|
+
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"
|
|
444
|
+
>
|
|
445
|
+
{FolderIcon}
|
|
446
|
+
Open Output Folder
|
|
447
|
+
</button>
|
|
448
|
+
|
|
449
|
+
{/* ===== Error ===== */}
|
|
450
|
+
{store.textToImage.error && (
|
|
451
|
+
<div className="p-5 bg-red-50 border border-red-200 rounded-2xl text-red-600 text-sm">
|
|
452
|
+
{store.textToImage.error}
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{/* ===== Logs ===== */}
|
|
457
|
+
{store.textToImage.logs.length > 0 && (
|
|
458
|
+
<div className="p-5 bg-ink-50 border border-ink-200 rounded-2xl max-h-40 overflow-y-auto">
|
|
459
|
+
<p className="text-xs font-semibold text-ink-700 uppercase tracking-wider mb-2">
|
|
460
|
+
Logs
|
|
461
|
+
</p>
|
|
462
|
+
<pre className="text-xs text-ink-600 font-mono whitespace-pre-wrap">
|
|
463
|
+
{store.textToImage.logs.join("")}
|
|
464
|
+
</pre>
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
|
|
468
|
+
{/* ===== Result ===== */}
|
|
469
|
+
{store.textToImage.result && (
|
|
470
|
+
<div>
|
|
471
|
+
<div className="flex items-center justify-between mb-2">
|
|
472
|
+
<label className="text-xs font-semibold text-ink-700 uppercase tracking-wider">
|
|
473
|
+
Generated Image
|
|
474
|
+
</label>
|
|
475
|
+
<button
|
|
476
|
+
onClick={() => store.clearTextToImageResult()}
|
|
477
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-red-600 hover:border-red-200 hover:bg-red-50 transition-colors"
|
|
478
|
+
>
|
|
479
|
+
{CloseIcon}
|
|
480
|
+
Remove
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
<div className="rounded-2xl overflow-hidden border border-ink-200 shadow-card inline-block">
|
|
484
|
+
<img
|
|
485
|
+
src={store.textToImage.result}
|
|
486
|
+
alt="Generated"
|
|
487
|
+
className="max-w-full h-auto"
|
|
488
|
+
/>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
{/* ===== Generated Images Grid ===== */}
|
|
494
|
+
<div>
|
|
495
|
+
<div className="flex items-center justify-between mb-2">
|
|
496
|
+
<label className="block text-xs font-semibold text-ink-700 uppercase tracking-wider">
|
|
497
|
+
Generated Images
|
|
498
|
+
</label>
|
|
499
|
+
<button
|
|
500
|
+
onClick={() => openFolder(projectId, "output")}
|
|
501
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-xl border border-ink-200 text-ink-600 hover:border-ink-300 hover:text-ink-900 transition-colors"
|
|
502
|
+
>
|
|
503
|
+
{FolderIcon}
|
|
504
|
+
Open Folder
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
{store.projectImagesLoading ? (
|
|
508
|
+
<p className="text-xs text-ink-500 italic py-4 text-center">
|
|
509
|
+
Loading images...
|
|
510
|
+
</p>
|
|
511
|
+
) : generatedImages.length === 0 ? (
|
|
512
|
+
<p className="text-xs text-ink-500 italic py-4 text-center border border-dashed border-ink-200 rounded-2xl">
|
|
513
|
+
No generated images yet.
|
|
514
|
+
</p>
|
|
515
|
+
) : (
|
|
516
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 gap-2 p-1">
|
|
517
|
+
{generatedImages.map((img) => {
|
|
518
|
+
const fullUrl = img.url.startsWith("http")
|
|
519
|
+
? img.url
|
|
520
|
+
: `http://localhost:${(window as any).PORT}${img.url}`;
|
|
521
|
+
return (
|
|
522
|
+
<button
|
|
523
|
+
key={`${img.source}-${img.filename}`}
|
|
524
|
+
onClick={() =>
|
|
525
|
+
setPreview({ url: fullUrl, filename: img.filename })
|
|
526
|
+
}
|
|
527
|
+
className="relative rounded-xl border border-ink-200 hover:border-tiffany-500 overflow-hidden transition-all cursor-zoom-in group"
|
|
528
|
+
>
|
|
529
|
+
<img
|
|
530
|
+
src={fullUrl}
|
|
531
|
+
alt={img.filename}
|
|
532
|
+
className="aspect-square object-cover object-center w-full"
|
|
533
|
+
/>
|
|
534
|
+
<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">
|
|
535
|
+
{img.filename}
|
|
536
|
+
</span>
|
|
537
|
+
</button>
|
|
538
|
+
);
|
|
539
|
+
})}
|
|
540
|
+
</div>
|
|
541
|
+
)}
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
{/* ===== Preview Modal ===== */}
|
|
545
|
+
{preview && (
|
|
546
|
+
<div
|
|
547
|
+
className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-8"
|
|
548
|
+
onClick={() => setPreview(null)}
|
|
549
|
+
>
|
|
550
|
+
<div
|
|
551
|
+
className="relative max-w-[90vw] max-h-[90vh] flex flex-col items-center gap-3"
|
|
552
|
+
onClick={(e) => e.stopPropagation()}
|
|
553
|
+
>
|
|
554
|
+
<img
|
|
555
|
+
src={preview.url}
|
|
556
|
+
alt={preview.filename}
|
|
557
|
+
className="max-w-full max-h-[80vh] object-contain rounded-2xl shadow-2xl"
|
|
558
|
+
/>
|
|
559
|
+
<span className="text-xs text-white/70 truncate max-w-full">
|
|
560
|
+
{preview.filename}
|
|
561
|
+
</span>
|
|
562
|
+
<button
|
|
563
|
+
onClick={() => setPreview(null)}
|
|
564
|
+
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"
|
|
565
|
+
title="Close (Esc)"
|
|
566
|
+
>
|
|
567
|
+
{CloseIcon}
|
|
568
|
+
</button>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
)}
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
}
|