openvoiceui 1.0.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/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- package/tts_providers/supertonic_tts.py +399 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Image Studio</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
12
|
+
background: #080d18;
|
|
13
|
+
color: #e2e8f0;
|
|
14
|
+
padding: 20px;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.page-header {
|
|
19
|
+
text-align: center;
|
|
20
|
+
margin-bottom: 28px;
|
|
21
|
+
padding-bottom: 20px;
|
|
22
|
+
border-bottom: 1px solid rgba(148,163,184,0.1);
|
|
23
|
+
}
|
|
24
|
+
.page-header h1 {
|
|
25
|
+
font-size: 1.9em;
|
|
26
|
+
font-weight: 700;
|
|
27
|
+
background: linear-gradient(90deg, #6366f1, #a78bfa, #38bdf8);
|
|
28
|
+
-webkit-background-clip: text;
|
|
29
|
+
-webkit-text-fill-color: transparent;
|
|
30
|
+
background-clip: text;
|
|
31
|
+
margin-bottom: 6px;
|
|
32
|
+
}
|
|
33
|
+
.page-header p { font-size: 0.86em; color: #64748b; }
|
|
34
|
+
|
|
35
|
+
/* ── Creator card ── */
|
|
36
|
+
.creator-card {
|
|
37
|
+
background: #111827;
|
|
38
|
+
border: 2px solid rgba(99,102,241,0.3);
|
|
39
|
+
border-radius: 16px;
|
|
40
|
+
padding: 22px;
|
|
41
|
+
margin-bottom: 32px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ── Option buttons ── */
|
|
45
|
+
.opt-row {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
gap: 6px;
|
|
49
|
+
margin-bottom: 12px;
|
|
50
|
+
align-items: center;
|
|
51
|
+
}
|
|
52
|
+
.opt-label {
|
|
53
|
+
font-size: 0.78em;
|
|
54
|
+
color: #64748b;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
min-width: 46px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.model-btn {
|
|
60
|
+
background: rgba(30,41,59,0.7);
|
|
61
|
+
border: 1.5px solid rgba(148,163,184,0.2);
|
|
62
|
+
color: #94a3b8;
|
|
63
|
+
padding: 5px 11px;
|
|
64
|
+
border-radius: 6px;
|
|
65
|
+
font-size: 0.81em;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
transition: all 0.15s;
|
|
68
|
+
}
|
|
69
|
+
.model-btn:hover { border-color: #818cf8; color: #818cf8; }
|
|
70
|
+
.model-btn.active { background: rgba(99,102,241,0.18); border-color: #818cf8; color: #a5b4fc; font-weight: 600; }
|
|
71
|
+
|
|
72
|
+
.opt-btn {
|
|
73
|
+
background: rgba(30,41,59,0.7);
|
|
74
|
+
border: 1.5px solid rgba(148,163,184,0.18);
|
|
75
|
+
color: #94a3b8;
|
|
76
|
+
padding: 4px 9px;
|
|
77
|
+
border-radius: 6px;
|
|
78
|
+
font-size: 0.77em;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
transition: all 0.15s;
|
|
81
|
+
}
|
|
82
|
+
.opt-btn:hover { border-color: #818cf8; color: #818cf8; }
|
|
83
|
+
.opt-btn.active { background: rgba(99,102,241,0.12); border-color: rgba(99,102,241,0.55); color: #a5b4fc; font-weight: 600; }
|
|
84
|
+
|
|
85
|
+
.opt-hint { font-size: 0.68em; color: #475569; }
|
|
86
|
+
|
|
87
|
+
/* ── Prompt + refs layout ── */
|
|
88
|
+
.prompt-refs-grid {
|
|
89
|
+
display: grid;
|
|
90
|
+
grid-template-columns: 1fr 240px;
|
|
91
|
+
gap: 16px;
|
|
92
|
+
margin-bottom: 16px;
|
|
93
|
+
}
|
|
94
|
+
@media (max-width: 680px) { .prompt-refs-grid { grid-template-columns: 1fr; } }
|
|
95
|
+
|
|
96
|
+
textarea#creator-prompt {
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 120px;
|
|
99
|
+
background: #0f172a;
|
|
100
|
+
border: 2px solid rgba(148,163,184,0.18);
|
|
101
|
+
border-radius: 10px;
|
|
102
|
+
padding: 12px;
|
|
103
|
+
color: #e2e8f0;
|
|
104
|
+
font-size: 0.93em;
|
|
105
|
+
resize: vertical;
|
|
106
|
+
font-family: inherit;
|
|
107
|
+
outline: none;
|
|
108
|
+
box-sizing: border-box;
|
|
109
|
+
transition: border-color 0.2s;
|
|
110
|
+
}
|
|
111
|
+
textarea#creator-prompt:focus { border-color: rgba(99,102,241,0.5); }
|
|
112
|
+
|
|
113
|
+
.prompt-meta {
|
|
114
|
+
display: flex;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
align-items: center;
|
|
117
|
+
margin-top: 6px;
|
|
118
|
+
}
|
|
119
|
+
.prompt-btns { display: flex; gap: 7px; }
|
|
120
|
+
|
|
121
|
+
.sm-btn {
|
|
122
|
+
background: rgba(30,41,59,0.7);
|
|
123
|
+
border: 1px solid rgba(148,163,184,0.18);
|
|
124
|
+
color: #94a3b8;
|
|
125
|
+
padding: 4px 9px;
|
|
126
|
+
border-radius: 5px;
|
|
127
|
+
font-size: 0.77em;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
transition: all 0.15s;
|
|
130
|
+
}
|
|
131
|
+
.sm-btn:hover { border-color: #60a5fa; color: #60a5fa; }
|
|
132
|
+
.sm-btn.purple { background: rgba(139,92,246,0.12); border-color: rgba(139,92,246,0.35); color: #a78bfa; }
|
|
133
|
+
.sm-btn.purple:hover { background: rgba(139,92,246,0.22); }
|
|
134
|
+
|
|
135
|
+
/* ── Reference image slots ── */
|
|
136
|
+
.ref-label { font-size: 0.77em; color: #64748b; margin-bottom: 6px; }
|
|
137
|
+
.ref-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
|
|
138
|
+
|
|
139
|
+
.ref-slot {
|
|
140
|
+
background: #0a1020;
|
|
141
|
+
border: 2px dashed rgba(148,163,184,0.18);
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
height: 70px;
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
position: relative;
|
|
150
|
+
transition: border-color 0.2s;
|
|
151
|
+
}
|
|
152
|
+
.ref-slot:hover { border-color: rgba(99,102,241,0.45); }
|
|
153
|
+
.ref-slot.loaded { border-style: solid; border-color: rgba(99,102,241,0.5); }
|
|
154
|
+
.ref-placeholder { color: #334155; font-size: 1.7em; font-weight: 300; }
|
|
155
|
+
.ref-slot img { width: 100%; height: 100%; object-fit: cover; }
|
|
156
|
+
.ref-remove { position: absolute; top: 2px; right: 4px; background: rgba(0,0,0,0.75); color: #ef4444; border: none; cursor: pointer; font-size: 0.85em; border-radius: 3px; padding: 1px 4px; display: none; }
|
|
157
|
+
.ref-slot.loaded .ref-remove { display: block; }
|
|
158
|
+
|
|
159
|
+
/* ── Generate button ── */
|
|
160
|
+
.gen-wrap { text-align: center; margin-bottom: 20px; }
|
|
161
|
+
#generate-btn {
|
|
162
|
+
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
163
|
+
color: #fff;
|
|
164
|
+
border: none;
|
|
165
|
+
padding: 13px 52px;
|
|
166
|
+
border-radius: 10px;
|
|
167
|
+
font-size: 1.08em;
|
|
168
|
+
font-weight: 700;
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
transition: all 0.2s;
|
|
171
|
+
box-shadow: 0 4px 20px rgba(99,102,241,0.35);
|
|
172
|
+
}
|
|
173
|
+
#generate-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(99,102,241,0.5); }
|
|
174
|
+
#generate-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; }
|
|
175
|
+
|
|
176
|
+
/* ── Loading ── */
|
|
177
|
+
#creator-loading { display: none; text-align: center; padding: 36px 20px; }
|
|
178
|
+
@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }
|
|
179
|
+
.spinner { font-size: 2.4em; animation: spin 1s linear infinite; display: inline-block; }
|
|
180
|
+
#load-msg { margin-top: 14px; font-size: 1.05em; color: #818cf8; font-weight: 600; }
|
|
181
|
+
|
|
182
|
+
/* ── Result ── */
|
|
183
|
+
#creator-result {
|
|
184
|
+
display: none;
|
|
185
|
+
border-top: 1px solid rgba(148,163,184,0.12);
|
|
186
|
+
padding-top: 20px;
|
|
187
|
+
}
|
|
188
|
+
.result-grid {
|
|
189
|
+
display: grid;
|
|
190
|
+
grid-template-columns: 1fr 210px;
|
|
191
|
+
gap: 20px;
|
|
192
|
+
align-items: start;
|
|
193
|
+
}
|
|
194
|
+
@media (max-width: 680px) { .result-grid { grid-template-columns: 1fr; } }
|
|
195
|
+
|
|
196
|
+
#result-img-wrap {
|
|
197
|
+
background: #030710;
|
|
198
|
+
border: 2px solid rgba(99,102,241,0.25);
|
|
199
|
+
border-radius: 12px;
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
justify-content: center;
|
|
204
|
+
min-height: 220px;
|
|
205
|
+
}
|
|
206
|
+
#result-img { max-width: 100%; max-height: 520px; object-fit: contain; cursor: zoom-in; }
|
|
207
|
+
#result-text { font-size: 0.8em; color: #64748b; margin-top: 7px; font-style: italic; }
|
|
208
|
+
.result-label { font-size: 0.8em; color: #64748b; margin-bottom: 7px; }
|
|
209
|
+
|
|
210
|
+
.result-actions { display: flex; flex-direction: column; gap: 9px; padding-top: 16px; }
|
|
211
|
+
.result-actions-title { font-size: 0.83em; color: #94a3b8; font-weight: 600; margin-bottom: 2px; }
|
|
212
|
+
|
|
213
|
+
.rab {
|
|
214
|
+
width: 100%;
|
|
215
|
+
border: 2px solid;
|
|
216
|
+
border-radius: 9px;
|
|
217
|
+
padding: 9px 11px;
|
|
218
|
+
font-size: 0.86em;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
text-align: left;
|
|
222
|
+
transition: all 0.15s;
|
|
223
|
+
background: transparent;
|
|
224
|
+
line-height: 1.3;
|
|
225
|
+
}
|
|
226
|
+
.rab .sub { font-size: 0.75em; font-weight: 400; opacity: 0.75; margin-top: 2px; }
|
|
227
|
+
.rab.green { border-color: rgba(34,197,94,0.45); color: #4ade80; }
|
|
228
|
+
.rab.green:hover { background: rgba(34,197,94,0.1); }
|
|
229
|
+
.rab.amber { border-color: rgba(245,158,11,0.45); color: #f59e0b; }
|
|
230
|
+
.rab.amber:hover { background: rgba(245,158,11,0.1); }
|
|
231
|
+
.rab.blue { border-color: rgba(96,165,250,0.45); color: #60a5fa; }
|
|
232
|
+
.rab.blue:hover { background: rgba(96,165,250,0.1); }
|
|
233
|
+
.rab.purple { border-color: rgba(167,139,250,0.45); color: #a78bfa; }
|
|
234
|
+
.rab.purple:hover { background: rgba(167,139,250,0.1); }
|
|
235
|
+
.rab.teal { border-color: rgba(20,184,166,0.45); color: #2dd4bf; }
|
|
236
|
+
.rab.teal:hover { background: rgba(20,184,166,0.1); }
|
|
237
|
+
.rab.gray { border-color: rgba(100,116,139,0.35); color: #94a3b8; }
|
|
238
|
+
.rab.gray:hover { background: rgba(100,116,139,0.1); }
|
|
239
|
+
|
|
240
|
+
/* ── Gallery ── */
|
|
241
|
+
.gallery-section { margin-top: 8px; }
|
|
242
|
+
.gallery-header {
|
|
243
|
+
display: flex;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
align-items: flex-end;
|
|
246
|
+
margin-bottom: 16px;
|
|
247
|
+
}
|
|
248
|
+
.gallery-title { font-size: 1.05em; font-weight: 700; color: #e2e8f0; }
|
|
249
|
+
.gallery-sub { font-size: 0.78em; color: #475569; margin-top: 2px; }
|
|
250
|
+
#gallery-count { font-size: 0.8em; color: #64748b; }
|
|
251
|
+
|
|
252
|
+
#gallery-grid {
|
|
253
|
+
display: grid;
|
|
254
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
255
|
+
gap: 14px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.gcard {
|
|
259
|
+
background: #111827;
|
|
260
|
+
border: 1.5px solid rgba(99,102,241,0.18);
|
|
261
|
+
border-radius: 12px;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
transition: border-color 0.2s, transform 0.2s;
|
|
264
|
+
}
|
|
265
|
+
.gcard:hover { border-color: rgba(99,102,241,0.5); transform: translateY(-2px); }
|
|
266
|
+
|
|
267
|
+
.gcard-img {
|
|
268
|
+
background: #060d1c;
|
|
269
|
+
aspect-ratio: 1;
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
justify-content: center;
|
|
273
|
+
overflow: hidden;
|
|
274
|
+
}
|
|
275
|
+
.gcard-img img { width: 100%; height: 100%; object-fit: contain; cursor: zoom-in; transition: transform 0.2s; }
|
|
276
|
+
.gcard-img img:hover { transform: scale(1.03); }
|
|
277
|
+
|
|
278
|
+
.gcard-body { padding: 9px 11px; }
|
|
279
|
+
.gcard-name {
|
|
280
|
+
font-size: 0.8em;
|
|
281
|
+
font-weight: 600;
|
|
282
|
+
color: #cbd5e1;
|
|
283
|
+
margin-bottom: 8px;
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
overflow: hidden;
|
|
286
|
+
text-overflow: ellipsis;
|
|
287
|
+
}
|
|
288
|
+
.gcard-actions { display: flex; gap: 5px; }
|
|
289
|
+
.gcard-actions button {
|
|
290
|
+
flex: 1;
|
|
291
|
+
background: rgba(30,41,59,0.7);
|
|
292
|
+
border: 1.5px solid rgba(148,163,184,0.18);
|
|
293
|
+
color: #94a3b8;
|
|
294
|
+
border-radius: 6px;
|
|
295
|
+
padding: 5px 4px;
|
|
296
|
+
font-size: 0.72em;
|
|
297
|
+
cursor: pointer;
|
|
298
|
+
transition: all 0.15s;
|
|
299
|
+
white-space: nowrap;
|
|
300
|
+
}
|
|
301
|
+
.gcard-actions button:hover { border-color: #818cf8; color: #a5b4fc; }
|
|
302
|
+
.gcard-actions button.del:hover { border-color: #f87171; color: #f87171; }
|
|
303
|
+
|
|
304
|
+
#gallery-empty {
|
|
305
|
+
grid-column: 1/-1;
|
|
306
|
+
text-align: center;
|
|
307
|
+
padding: 50px 20px;
|
|
308
|
+
color: #334155;
|
|
309
|
+
font-size: 0.9em;
|
|
310
|
+
border: 2px dashed rgba(148,163,184,0.1);
|
|
311
|
+
border-radius: 12px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ── Lightbox ── */
|
|
315
|
+
#lightbox {
|
|
316
|
+
display: none;
|
|
317
|
+
position: fixed;
|
|
318
|
+
inset: 0;
|
|
319
|
+
background: rgba(0,0,0,0.92);
|
|
320
|
+
z-index: 9999;
|
|
321
|
+
align-items: center;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
cursor: zoom-out;
|
|
324
|
+
flex-direction: column;
|
|
325
|
+
gap: 16px;
|
|
326
|
+
}
|
|
327
|
+
#lightbox.open { display: flex; }
|
|
328
|
+
#lightbox img { max-width: 90vw; max-height: 85vh; object-fit: contain; border-radius: 8px; }
|
|
329
|
+
#lightbox-actions { display: flex; gap: 10px; }
|
|
330
|
+
#lightbox-actions button {
|
|
331
|
+
background: rgba(99,102,241,0.2);
|
|
332
|
+
border: 1.5px solid rgba(99,102,241,0.5);
|
|
333
|
+
color: #a5b4fc;
|
|
334
|
+
padding: 8px 18px;
|
|
335
|
+
border-radius: 8px;
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
font-size: 0.88em;
|
|
338
|
+
font-weight: 600;
|
|
339
|
+
transition: all 0.15s;
|
|
340
|
+
}
|
|
341
|
+
#lightbox-actions button:hover { background: rgba(99,102,241,0.35); }
|
|
342
|
+
#lightbox-actions button.close-btn { background: rgba(100,116,139,0.2); border-color: rgba(100,116,139,0.4); color: #94a3b8; }
|
|
343
|
+
|
|
344
|
+
/* ── Toast ── */
|
|
345
|
+
#toast {
|
|
346
|
+
position: fixed;
|
|
347
|
+
bottom: 22px;
|
|
348
|
+
left: 50%;
|
|
349
|
+
transform: translateX(-50%) translateY(80px);
|
|
350
|
+
background: #1e293b;
|
|
351
|
+
border: 1px solid rgba(148,163,184,0.2);
|
|
352
|
+
border-radius: 8px;
|
|
353
|
+
padding: 9px 20px;
|
|
354
|
+
font-size: 0.86em;
|
|
355
|
+
color: #e2e8f0;
|
|
356
|
+
transition: transform 0.3s ease;
|
|
357
|
+
z-index: 8888;
|
|
358
|
+
white-space: nowrap;
|
|
359
|
+
pointer-events: none;
|
|
360
|
+
}
|
|
361
|
+
#toast.show { transform: translateX(-50%) translateY(0); }
|
|
362
|
+
#toast.ok { border-color: rgba(34,197,94,0.5); color: #4ade80; }
|
|
363
|
+
#toast.err { border-color: rgba(239,68,68,0.5); color: #f87171; }
|
|
364
|
+
#toast.info { border-color: rgba(99,102,241,0.5); color: #a5b4fc; }
|
|
365
|
+
</style>
|
|
366
|
+
</head>
|
|
367
|
+
<body>
|
|
368
|
+
|
|
369
|
+
<div class="page-header">
|
|
370
|
+
<h1>✨ AI Image Studio</h1>
|
|
371
|
+
<p>Generate · Edit · Save · Download — powered by Nano Banana Pro, Imagen 4, and Gemini Flash</p>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<!-- ═══════════ CREATOR ═══════════ -->
|
|
375
|
+
<div class="creator-card">
|
|
376
|
+
|
|
377
|
+
<!-- Model -->
|
|
378
|
+
<div class="opt-row">
|
|
379
|
+
<span class="opt-label">Google</span>
|
|
380
|
+
<button class="model-btn active" data-model="nano-banana-pro-preview" onclick="setModel(this)">🍌 Nano Banana Pro</button>
|
|
381
|
+
<button class="model-btn" data-model="gemini-3.1-flash-image-preview" onclick="setModel(this)">⚡ Gemini 3.1 Flash</button>
|
|
382
|
+
<button class="model-btn" data-model="imagen-4.0-generate-001" onclick="setModel(this)">🖼️ Imagen 4</button>
|
|
383
|
+
<button class="model-btn" data-model="imagen-4.0-fast-generate-001" onclick="setModel(this)">🚀 Imagen 4 Fast</button>
|
|
384
|
+
<button class="model-btn" data-model="gemini-2.0-flash-exp-image-generation" onclick="setModel(this)">🔬 Flash Exp</button>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="opt-row">
|
|
387
|
+
<span class="opt-label">HF</span>
|
|
388
|
+
<button class="model-btn" data-model="hf:black-forest-labs/FLUX.1-schnell" onclick="setModel(this)">⚡ FLUX Schnell</button>
|
|
389
|
+
<button class="model-btn" data-model="hf:black-forest-labs/FLUX.1-dev" onclick="setModel(this)">🎨 FLUX Dev</button>
|
|
390
|
+
<button class="model-btn" data-model="hf:stabilityai/stable-diffusion-xl-base-1.0" onclick="setModel(this)">🖼️ SDXL</button>
|
|
391
|
+
<button class="model-btn" data-model="hf:stabilityai/stable-diffusion-3.5-large" onclick="setModel(this)">🌟 SD 3.5 Large</button>
|
|
392
|
+
<button class="model-btn" data-model="hf:stabilityai/stable-diffusion-3.5-large-turbo" onclick="setModel(this)">🚀 SD 3.5 Turbo</button>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<!-- Style -->
|
|
396
|
+
<div class="opt-row">
|
|
397
|
+
<span class="opt-label">Style</span>
|
|
398
|
+
<button class="opt-btn active" data-style="" onclick="setStyle(this)">Auto</button>
|
|
399
|
+
<button class="opt-btn" data-style="vintage tattoo flash art, bold outlines, fully colored, classic tattoo palette" onclick="setStyle(this)">🐉 Tattoo Flash</button>
|
|
400
|
+
<button class="opt-btn" data-style="retro 80s airbrush illustration, vibrant gradients, chrome text, neon colors" onclick="setStyle(this)">💡 80s Airbrush</button>
|
|
401
|
+
<button class="opt-btn" data-style="comic book illustration, ink outlines, vibrant flat colors, dynamic action pose" onclick="setStyle(this)">💥 Comic Book</button>
|
|
402
|
+
<button class="opt-btn" data-style="photorealistic digital render, cinematic lighting, film grain, ultra detailed" onclick="setStyle(this)">🎬 Cinematic</button>
|
|
403
|
+
<button class="opt-btn" data-style="heavy metal album cover art, dramatic lighting, dark and epic" onclick="setStyle(this)">🤘 Metal</button>
|
|
404
|
+
<button class="opt-btn" data-style="graffiti mural art, urban texture, spray paint style, bold outlines" onclick="setStyle(this)">🌆 Graffiti</button>
|
|
405
|
+
<button class="opt-btn" data-style="old-school hot rod pinstripe art, flames, chrome, retro americana" onclick="setStyle(this)">🔥 Hot Rod</button>
|
|
406
|
+
<button class="opt-btn" data-style="minimalist vector illustration, clean lines, flat design, bold shapes, limited palette" onclick="setStyle(this)">◼ Flat / Vector</button>
|
|
407
|
+
<button class="opt-btn" data-style="watercolor painting, soft edges, flowing color washes, artistic texture, impressionistic" onclick="setStyle(this)">🎨 Watercolor</button>
|
|
408
|
+
<button class="opt-btn" data-style="pixel art, 16-bit retro game style, vibrant colors, clean pixels" onclick="setStyle(this)">👾 Pixel Art</button>
|
|
409
|
+
<button class="opt-btn" data-style="sticker art style, thick white outline, bold flat colors, clean background, die-cut ready" onclick="setStyle(this)">⭐ Sticker</button>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<!-- Aspect + Quality -->
|
|
413
|
+
<div class="opt-row">
|
|
414
|
+
<span class="opt-label">Aspect</span>
|
|
415
|
+
<button class="opt-btn active" data-aspect="1:1" onclick="setAspect(this)">◼ 1:1 Square</button>
|
|
416
|
+
<button class="opt-btn" data-aspect="3:4" onclick="setAspect(this)">▯ 3:4 Portrait</button>
|
|
417
|
+
<button class="opt-btn" data-aspect="4:3" onclick="setAspect(this)">▭ 4:3 Landscape</button>
|
|
418
|
+
<button class="opt-btn" data-aspect="16:9" onclick="setAspect(this)">― 16:9 Wide</button>
|
|
419
|
+
</div>
|
|
420
|
+
<div class="opt-row">
|
|
421
|
+
<span class="opt-label">Quality</span>
|
|
422
|
+
<button class="opt-btn active" data-quality="standard" onclick="setQuality(this)">1K (1024px)</button>
|
|
423
|
+
<button class="opt-btn" data-quality="high" onclick="setQuality(this)">⬆ 1.5K (1536px)</button>
|
|
424
|
+
<button class="opt-btn" data-quality="ultra" onclick="setQuality(this)">★ 2K (2048px)</button>
|
|
425
|
+
<span class="opt-hint"> HF models: actual resolution · Google models: prompt hint only</span>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<!-- Prompt + Refs -->
|
|
429
|
+
<div class="prompt-refs-grid">
|
|
430
|
+
<div>
|
|
431
|
+
<textarea id="creator-prompt" placeholder="Describe the image you want… e.g. 'a robot playing chess in a neon-lit café, cyberpunk style, t-shirt graphic, dark background'" oninput="updateCharCount(this)"></textarea>
|
|
432
|
+
<div class="prompt-meta">
|
|
433
|
+
<div style="display:flex;align-items:center;gap:10px;">
|
|
434
|
+
<span id="char-count" style="font-size:0.74em;color:#475569;">0 chars</span>
|
|
435
|
+
<span id="enhance-status" style="display:none;font-size:0.74em;color:#a78bfa;">⏳ Enhancing...</span>
|
|
436
|
+
</div>
|
|
437
|
+
<div class="prompt-btns">
|
|
438
|
+
<button class="sm-btn" onclick="clearPrompt()">Clear</button>
|
|
439
|
+
<button class="sm-btn" onclick="randomPrompt()">✨ Inspire</button>
|
|
440
|
+
<button class="sm-btn purple" id="enhance-btn" onclick="enhancePrompt()">🦥 Enhance</button>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
<div>
|
|
445
|
+
<div class="ref-label">Reference images — upload to edit or combine (optional)</div>
|
|
446
|
+
<div class="ref-grid">
|
|
447
|
+
<div class="ref-slot" id="ref-0" onclick="document.getElementById('ref-input-0').click()">
|
|
448
|
+
<input type="file" id="ref-input-0" accept="image/*" style="display:none" onchange="loadRefImage(0,this)">
|
|
449
|
+
<div class="ref-placeholder">+</div>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="ref-slot" id="ref-1" onclick="document.getElementById('ref-input-1').click()">
|
|
452
|
+
<input type="file" id="ref-input-1" accept="image/*" style="display:none" onchange="loadRefImage(1,this)">
|
|
453
|
+
<div class="ref-placeholder">+</div>
|
|
454
|
+
</div>
|
|
455
|
+
<div class="ref-slot" id="ref-2" onclick="document.getElementById('ref-input-2').click()">
|
|
456
|
+
<input type="file" id="ref-input-2" accept="image/*" style="display:none" onchange="loadRefImage(2,this)">
|
|
457
|
+
<div class="ref-placeholder">+</div>
|
|
458
|
+
</div>
|
|
459
|
+
<div class="ref-slot" id="ref-3" onclick="document.getElementById('ref-input-3').click()">
|
|
460
|
+
<input type="file" id="ref-input-3" accept="image/*" style="display:none" onchange="loadRefImage(3,this)">
|
|
461
|
+
<div class="ref-placeholder">+</div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
<button class="sm-btn" style="margin-top:7px;width:100%;" onclick="clearRefImages()">Clear all refs</button>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Generate -->
|
|
469
|
+
<div class="gen-wrap">
|
|
470
|
+
<button id="generate-btn" onclick="generateImage()">✨ Generate Image</button>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<!-- Loading -->
|
|
474
|
+
<div id="creator-loading">
|
|
475
|
+
<div class="spinner">🍌</div>
|
|
476
|
+
<div id="load-msg">Generating your image…</div>
|
|
477
|
+
<div style="margin-top:7px;font-size:0.83em;color:#475569;">Usually 5–20 seconds depending on model</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<!-- Result -->
|
|
481
|
+
<div id="creator-result">
|
|
482
|
+
<div class="result-grid">
|
|
483
|
+
<div>
|
|
484
|
+
<div class="result-label">Generated image — click to zoom</div>
|
|
485
|
+
<div id="result-img-wrap">
|
|
486
|
+
<img id="result-img" onclick="openLightbox(this.src, currentResultName())" />
|
|
487
|
+
</div>
|
|
488
|
+
<div id="result-text"></div>
|
|
489
|
+
</div>
|
|
490
|
+
<div class="result-actions">
|
|
491
|
+
<div class="result-actions-title">Actions</div>
|
|
492
|
+
<button class="rab green" onclick="saveToGallery()">
|
|
493
|
+
💾 Save to Gallery
|
|
494
|
+
<div class="sub">Saves to server — always available</div>
|
|
495
|
+
</button>
|
|
496
|
+
<button class="rab teal" onclick="copyResultUrl()">
|
|
497
|
+
📋 Copy Image URL
|
|
498
|
+
<div class="sub">Copy server URL to clipboard</div>
|
|
499
|
+
</button>
|
|
500
|
+
<button class="rab purple" onclick="downloadResult()">
|
|
501
|
+
⬇️ Download
|
|
502
|
+
<div class="sub">Save image to your device</div>
|
|
503
|
+
</button>
|
|
504
|
+
<button class="rab amber" onclick="tweakImage()">
|
|
505
|
+
🔧 Use in Prompt
|
|
506
|
+
<div class="sub">Load as reference for next generation</div>
|
|
507
|
+
</button>
|
|
508
|
+
<button class="rab blue" onclick="generateVariation()">
|
|
509
|
+
🔀 Generate Variation
|
|
510
|
+
<div class="sub">Same prompt, new generation</div>
|
|
511
|
+
</button>
|
|
512
|
+
<button class="rab gray" onclick="clearResult()">
|
|
513
|
+
✕ Clear
|
|
514
|
+
<div class="sub">Start fresh</div>
|
|
515
|
+
</button>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
</div><!-- /creator-card -->
|
|
521
|
+
|
|
522
|
+
<!-- ═══════════ GALLERY ═══════════ -->
|
|
523
|
+
<div class="gallery-section">
|
|
524
|
+
<div class="gallery-header">
|
|
525
|
+
<div>
|
|
526
|
+
<div class="gallery-title">💾 Saved Images</div>
|
|
527
|
+
<div class="gallery-sub">Stored on server · Persistent across sessions</div>
|
|
528
|
+
</div>
|
|
529
|
+
<span id="gallery-count"></span>
|
|
530
|
+
</div>
|
|
531
|
+
<div id="gallery-grid">
|
|
532
|
+
<div id="gallery-empty">Generate an image and click <strong>Save to Gallery</strong> to see it here</div>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<!-- Lightbox -->
|
|
537
|
+
<div id="lightbox" onclick="closeLightboxOnBg(event)">
|
|
538
|
+
<img id="lightbox-img" src="" alt="">
|
|
539
|
+
<div id="lightbox-actions" onclick="event.stopPropagation()">
|
|
540
|
+
<button onclick="copyLightboxUrl()">📋 Copy URL</button>
|
|
541
|
+
<button onclick="downloadLightboxImg()">⬇️ Download</button>
|
|
542
|
+
<button onclick="useLightboxAsRef()">🔧 Use in Prompt</button>
|
|
543
|
+
<button class="close-btn" onclick="closeLightbox()">✕ Close</button>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
<!-- Toast -->
|
|
548
|
+
<div id="toast"></div>
|
|
549
|
+
|
|
550
|
+
<script>
|
|
551
|
+
// ── State ──────────────────────────────────────────────────────────
|
|
552
|
+
let selectedModel = 'nano-banana-pro-preview';
|
|
553
|
+
let selectedStyle = '';
|
|
554
|
+
let selectedAspect = '1:1';
|
|
555
|
+
let selectedQuality = 'standard';
|
|
556
|
+
let refImages = [null, null, null, null]; // { mime_type, data, dataUrl }
|
|
557
|
+
let lastResult = null; // { dataUrl, base64, mime, name }
|
|
558
|
+
|
|
559
|
+
const INSPIRE_PROMPTS = [
|
|
560
|
+
"A cyberpunk robot hacker sitting at a terminal in a neon-lit underground bunker, holographic code streams, dark background, t-shirt graphic style, ultra detailed",
|
|
561
|
+
"Retro 80s airbrush illustration of a velociraptor playing electric guitar on stage, neon pink and cyan, chrome lettering 'ROCK OR DIE', dark background",
|
|
562
|
+
"Vintage tattoo flash art: a great white shark wearing sunglasses, bold outlines, fully colored in classic tattoo palette, off-white background",
|
|
563
|
+
"Comic book panel: a heroic corgi in superhero costume flying over a city skyline, bold ink outlines, vibrant flat colors, dynamic upward angle",
|
|
564
|
+
"Heavy metal album cover: a skeleton astronaut floating through a galaxy of neon nebulae, dramatic back-lighting, 'VOID WALKER' typography space",
|
|
565
|
+
"Pixel art 16-bit: a wizard casting a lightning spell in a dungeon, glowing runes, treasure chests, torchlight, classic JRPG aesthetic",
|
|
566
|
+
"Graffiti mural: giant portrait of a T-rex in a NASA spacesuit, urban brick wall texture, vivid spray paint colors, bold black outlines",
|
|
567
|
+
"Sticker art: a cheerful alien barista making coffee, thick white outline, bright flat colors, transparent background ready, die-cut style",
|
|
568
|
+
"Classic tattoo flash sheet: anchor, swallow, skull with roses, banner reading 'FOREVER', bold outlines, traditional red and green palette",
|
|
569
|
+
"Minimalist vector: a fox in a business suit holding a briefcase, dark navy background, single color highlight in orange, clean geometric style",
|
|
570
|
+
"Watercolor: a mythical koi fish leaping through cherry blossoms, soft pinks and blues, flowing ink outlines, traditional Japanese aesthetic",
|
|
571
|
+
"Old-school hot rod illustration: a flaming skull driving a 1955 chopped rod, chrome engine exposed, desert highway, pinstripe art style",
|
|
572
|
+
"80s retro sci-fi poster: an astronaut surfing on the rings of Saturn, vivid gradient sunset, chrome retro font 'RADICAL COSMOS'",
|
|
573
|
+
"Comic book villain: a sentient pizza slice wearing a monocle and top hat, world domination map in background, dramatic cape, bold colors",
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
// ── Option setters ─────────────────────────────────────────────────
|
|
577
|
+
function setModel(btn) { document.querySelectorAll('.model-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); selectedModel=btn.dataset.model; }
|
|
578
|
+
function setStyle(btn) { document.querySelectorAll('[data-style]').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); selectedStyle=btn.dataset.style; }
|
|
579
|
+
function setAspect(btn) { document.querySelectorAll('[data-aspect]').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); selectedAspect=btn.dataset.aspect; }
|
|
580
|
+
function setQuality(btn) { document.querySelectorAll('[data-quality]').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); selectedQuality=btn.dataset.quality; }
|
|
581
|
+
|
|
582
|
+
// ── Prompt ─────────────────────────────────────────────────────────
|
|
583
|
+
function updateCharCount(ta) { document.getElementById('char-count').textContent = ta.value.length + ' chars'; }
|
|
584
|
+
function clearPrompt() { const ta=document.getElementById('creator-prompt'); ta.value=''; document.getElementById('char-count').textContent='0 chars'; }
|
|
585
|
+
function randomPrompt() { const p=INSPIRE_PROMPTS[Math.floor(Math.random()*INSPIRE_PROMPTS.length)]; document.getElementById('creator-prompt').value=p; document.getElementById('char-count').textContent=p.length+' chars'; }
|
|
586
|
+
|
|
587
|
+
async function enhancePrompt() {
|
|
588
|
+
const idea = document.getElementById('creator-prompt').value.trim();
|
|
589
|
+
if (!idea) { toast('Type your rough idea first, then click Enhance!', 'err'); return; }
|
|
590
|
+
const btn = document.getElementById('enhance-btn');
|
|
591
|
+
btn.disabled = true; btn.textContent = '...';
|
|
592
|
+
document.getElementById('enhance-status').style.display = 'inline';
|
|
593
|
+
try {
|
|
594
|
+
const resp = await fetch('/api/image-gen/enhance', {
|
|
595
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
596
|
+
body: JSON.stringify({ idea, quality: selectedQuality, style: selectedStyle }),
|
|
597
|
+
});
|
|
598
|
+
const r = await resp.json();
|
|
599
|
+
if (!resp.ok || r.error) { toast('❌ Enhance failed: '+(r.error||'Unknown error'), 'err'); return; }
|
|
600
|
+
document.getElementById('creator-prompt').value = r.prompt;
|
|
601
|
+
document.getElementById('char-count').textContent = r.prompt.length + ' chars';
|
|
602
|
+
toast('✨ Prompt enhanced — review and generate!', 'ok');
|
|
603
|
+
} catch(e) { toast('❌ '+e.message, 'err'); }
|
|
604
|
+
finally { btn.disabled=false; btn.textContent='🦥 Enhance'; document.getElementById('enhance-status').style.display='none'; }
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ── Reference images ───────────────────────────────────────────────
|
|
608
|
+
function loadRefImage(index, input) {
|
|
609
|
+
if (!input.files[0]) return;
|
|
610
|
+
const reader = new FileReader();
|
|
611
|
+
reader.onload = e => {
|
|
612
|
+
const dataUrl = e.target.result;
|
|
613
|
+
refImages[index] = { mime_type: input.files[0].type||'image/png', data: dataUrl.split(',')[1], dataUrl };
|
|
614
|
+
const slot = document.getElementById('ref-'+index);
|
|
615
|
+
slot.classList.add('loaded');
|
|
616
|
+
slot.innerHTML = `
|
|
617
|
+
<input type="file" id="ref-input-${index}" accept="image/*" style="display:none" onchange="loadRefImage(${index},this)">
|
|
618
|
+
<img src="${dataUrl}">
|
|
619
|
+
<button class="ref-remove" onclick="removeRef(event,${index})">✕</button>
|
|
620
|
+
`;
|
|
621
|
+
};
|
|
622
|
+
reader.readAsDataURL(input.files[0]);
|
|
623
|
+
}
|
|
624
|
+
function removeRef(e, index) {
|
|
625
|
+
e.stopPropagation();
|
|
626
|
+
refImages[index] = null;
|
|
627
|
+
const slot = document.getElementById('ref-'+index);
|
|
628
|
+
slot.classList.remove('loaded');
|
|
629
|
+
slot.innerHTML = `
|
|
630
|
+
<input type="file" id="ref-input-${index}" accept="image/*" style="display:none" onchange="loadRefImage(${index},this)">
|
|
631
|
+
<div class="ref-placeholder">+</div>
|
|
632
|
+
`;
|
|
633
|
+
slot.onclick = () => document.getElementById('ref-input-'+index).click();
|
|
634
|
+
}
|
|
635
|
+
function clearRefImages() { for(let i=0;i<4;i++) removeRef({stopPropagation:()=>{}},i); }
|
|
636
|
+
|
|
637
|
+
async function loadUrlAsRef(url, name) {
|
|
638
|
+
const slotIdx = refImages.findIndex(r=>!r);
|
|
639
|
+
const idx = slotIdx >= 0 ? slotIdx : 0;
|
|
640
|
+
try {
|
|
641
|
+
const resp = await fetch(url);
|
|
642
|
+
const blob = await resp.blob();
|
|
643
|
+
const mime = blob.type || 'image/png';
|
|
644
|
+
const reader = new FileReader();
|
|
645
|
+
reader.onload = e => {
|
|
646
|
+
const dataUrl = e.target.result;
|
|
647
|
+
refImages[idx] = { mime_type: mime, data: dataUrl.split(',')[1], dataUrl };
|
|
648
|
+
const slot = document.getElementById('ref-'+idx);
|
|
649
|
+
slot.classList.add('loaded');
|
|
650
|
+
slot.innerHTML = `
|
|
651
|
+
<input type="file" id="ref-input-${idx}" accept="image/*" style="display:none" onchange="loadRefImage(${idx},this)">
|
|
652
|
+
<img src="${dataUrl}">
|
|
653
|
+
<button class="ref-remove" onclick="removeRef(event,${idx})">✕</button>
|
|
654
|
+
`;
|
|
655
|
+
document.querySelector('.creator-card').scrollIntoView({behavior:'smooth',block:'start'});
|
|
656
|
+
setTimeout(()=>document.getElementById('creator-prompt').focus(), 400);
|
|
657
|
+
toast(`"${name}" loaded as reference`, 'info');
|
|
658
|
+
};
|
|
659
|
+
reader.readAsDataURL(blob);
|
|
660
|
+
} catch(e) { toast('❌ Could not load image: '+e.message, 'err'); }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Drag-and-drop
|
|
664
|
+
['ref-0','ref-1','ref-2','ref-3'].forEach((id,i)=>{
|
|
665
|
+
const slot = document.getElementById(id);
|
|
666
|
+
if (!slot) return;
|
|
667
|
+
slot.addEventListener('dragover', e => { e.preventDefault(); slot.style.borderColor='#818cf8'; });
|
|
668
|
+
slot.addEventListener('dragleave', ()=>{ slot.style.borderColor=''; });
|
|
669
|
+
slot.addEventListener('drop', e => {
|
|
670
|
+
e.preventDefault(); slot.style.borderColor='';
|
|
671
|
+
const file = e.dataTransfer.files[0];
|
|
672
|
+
if (!file||!file.type.startsWith('image/')) return;
|
|
673
|
+
loadRefImage(i, {files:[file]});
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// ── Generate ───────────────────────────────────────────────────────
|
|
678
|
+
async function generateImage() {
|
|
679
|
+
let prompt = document.getElementById('creator-prompt').value.trim();
|
|
680
|
+
if (!prompt) { toast('Please enter a prompt first!', 'err'); return; }
|
|
681
|
+
|
|
682
|
+
if (selectedStyle) prompt += ', ' + selectedStyle;
|
|
683
|
+
|
|
684
|
+
// For Google models, append quality hints to the prompt (only way to influence resolution)
|
|
685
|
+
const isHF = selectedModel.startsWith('hf:');
|
|
686
|
+
if (!isHF) {
|
|
687
|
+
if (selectedQuality==='high' && !/high.res|high resolution/i.test(prompt))
|
|
688
|
+
prompt += ', high-resolution print quality, sharp fine detail';
|
|
689
|
+
else if (selectedQuality==='ultra' && !/ultra/i.test(prompt))
|
|
690
|
+
prompt += ', ultra-high-resolution, photorealistic detail, maximum sharpness';
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const imgs = refImages.filter(Boolean).map(r=>({mime_type:r.mime_type, data:r.data}));
|
|
694
|
+
if ((selectedModel.startsWith('imagen-') || isHF) && imgs.length>0)
|
|
695
|
+
if (!confirm('This model is text-to-image only and ignores reference images. Continue without them?')) return;
|
|
696
|
+
|
|
697
|
+
document.getElementById('creator-result').style.display = 'none';
|
|
698
|
+
document.getElementById('creator-loading').style.display = 'block';
|
|
699
|
+
document.getElementById('generate-btn').disabled = true;
|
|
700
|
+
|
|
701
|
+
const msgs = ['Generating your image…','Applying model magic…','Almost there…','Rendering final details…'];
|
|
702
|
+
let mi = 0;
|
|
703
|
+
const msgTimer = setInterval(()=>{ document.getElementById('load-msg').textContent = msgs[mi++%msgs.length]; }, 4000);
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
const resp = await fetch('/api/image-gen', {
|
|
707
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
708
|
+
body: JSON.stringify({ prompt, images:imgs, model:selectedModel, aspect:selectedAspect, quality:selectedQuality }),
|
|
709
|
+
});
|
|
710
|
+
const result = await resp.json();
|
|
711
|
+
if (!resp.ok || result.error) { toast('❌ '+(result.error||'Generation failed'), 'err'); return; }
|
|
712
|
+
|
|
713
|
+
const img = result.images[0];
|
|
714
|
+
const originalPrompt = document.getElementById('creator-prompt').value.trim();
|
|
715
|
+
lastResult = {
|
|
716
|
+
dataUrl: img.url || `data:${img.mime_type};base64,${img.data}`,
|
|
717
|
+
base64: img.data,
|
|
718
|
+
mime: img.mime_type,
|
|
719
|
+
name: originalPrompt.substring(0,50).replace(/[^a-zA-Z0-9 ]/g,'').trim() || 'AI Image',
|
|
720
|
+
};
|
|
721
|
+
if (!img.url) console.error('[ImageGen] No server url returned — image may be lost on page refresh');
|
|
722
|
+
else console.log('[ImageGen] Saved to server:', img.url);
|
|
723
|
+
|
|
724
|
+
document.getElementById('result-img').src = lastResult.dataUrl;
|
|
725
|
+
document.getElementById('result-text').textContent = result.text || '';
|
|
726
|
+
document.getElementById('creator-result').style.display = 'block';
|
|
727
|
+
} catch(e) {
|
|
728
|
+
toast('❌ Network error: '+e.message, 'err');
|
|
729
|
+
} finally {
|
|
730
|
+
clearInterval(msgTimer);
|
|
731
|
+
document.getElementById('creator-loading').style.display = 'none';
|
|
732
|
+
document.getElementById('generate-btn').disabled = false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function currentResultName() { return lastResult?.name || 'AI Image'; }
|
|
737
|
+
function generateVariation() { generateImage(); }
|
|
738
|
+
|
|
739
|
+
function tweakImage() {
|
|
740
|
+
if (!lastResult) return;
|
|
741
|
+
refImages[0] = { mime_type: lastResult.mime||'image/png', data: lastResult.base64, dataUrl: lastResult.dataUrl };
|
|
742
|
+
const slot = document.getElementById('ref-0');
|
|
743
|
+
slot.classList.add('loaded');
|
|
744
|
+
slot.innerHTML = `
|
|
745
|
+
<input type="file" id="ref-input-0" accept="image/*" style="display:none" onchange="loadRefImage(0,this)">
|
|
746
|
+
<img src="${lastResult.dataUrl}">
|
|
747
|
+
<button class="ref-remove" onclick="removeRef(event,0)">✕</button>
|
|
748
|
+
`;
|
|
749
|
+
document.getElementById('creator-result').style.display = 'none';
|
|
750
|
+
document.getElementById('creator-prompt').scrollIntoView({behavior:'smooth'});
|
|
751
|
+
setTimeout(()=>document.getElementById('creator-prompt').focus(), 300);
|
|
752
|
+
toast('Image loaded as reference — update the prompt to edit it', 'info');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function clearResult() {
|
|
756
|
+
document.getElementById('creator-result').style.display = 'none';
|
|
757
|
+
clearPrompt(); clearRefImages();
|
|
758
|
+
lastResult = null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function copyResultUrl() {
|
|
762
|
+
if (!lastResult) return;
|
|
763
|
+
if (lastResult.dataUrl.startsWith('data:')) { toast('⚠️ Image not yet saved to server', 'err'); return; }
|
|
764
|
+
try { await navigator.clipboard.writeText(window.location.origin + lastResult.dataUrl); toast('📋 URL copied!', 'ok'); }
|
|
765
|
+
catch(e) { toast('Copy failed — try right-clicking the image', 'err'); }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ── Download ───────────────────────────────────────────────────────
|
|
769
|
+
function _b64ToBlob(b64, mime) {
|
|
770
|
+
const bytes = atob(b64), arr = new Uint8Array(bytes.length);
|
|
771
|
+
for (let i=0;i<bytes.length;i++) arr[i]=bytes.charCodeAt(i);
|
|
772
|
+
return new Blob([arr],{type:mime});
|
|
773
|
+
}
|
|
774
|
+
async function downloadFromUrl(url, name) {
|
|
775
|
+
try {
|
|
776
|
+
const resp = await fetch(url);
|
|
777
|
+
const blob = await resp.blob();
|
|
778
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
779
|
+
const a = document.createElement('a');
|
|
780
|
+
a.href = blobUrl; a.download = name;
|
|
781
|
+
document.body.appendChild(a); a.click();
|
|
782
|
+
document.body.removeChild(a);
|
|
783
|
+
setTimeout(()=>URL.revokeObjectURL(blobUrl), 2000);
|
|
784
|
+
} catch(e) { window.open(url,'_blank'); }
|
|
785
|
+
}
|
|
786
|
+
function downloadResult() {
|
|
787
|
+
if (!lastResult) return;
|
|
788
|
+
const ext = (lastResult.mime||'image/png').split('/')[1]||'png';
|
|
789
|
+
const name = (lastResult.name||'ai-image').replace(/\s+/g,'-') + '-' + Date.now() + '.' + ext;
|
|
790
|
+
if (lastResult.dataUrl.startsWith('data:')) {
|
|
791
|
+
const blob = _b64ToBlob(lastResult.base64, lastResult.mime||'image/png');
|
|
792
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
793
|
+
const a = document.createElement('a');
|
|
794
|
+
a.href=blobUrl; a.download=name;
|
|
795
|
+
document.body.appendChild(a); a.click();
|
|
796
|
+
document.body.removeChild(a);
|
|
797
|
+
setTimeout(()=>URL.revokeObjectURL(blobUrl), 2000);
|
|
798
|
+
} else { downloadFromUrl(lastResult.dataUrl, name); }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ── Gallery ────────────────────────────────────────────────────────
|
|
802
|
+
let galleryItems = [];
|
|
803
|
+
|
|
804
|
+
function _galleryCard(entry) {
|
|
805
|
+
const el = document.createElement('div');
|
|
806
|
+
el.className = 'gcard';
|
|
807
|
+
el.dataset.url = entry.url;
|
|
808
|
+
const sUrl = (entry.url||'').replace(/'/g,"\\'");
|
|
809
|
+
const sName = (entry.name||'AI Image').replace(/'/g,"\\'");
|
|
810
|
+
el.innerHTML = `
|
|
811
|
+
<div class="gcard-img">
|
|
812
|
+
<img src="${entry.url}" alt="${entry.name||'AI Image'}" loading="lazy" onclick="openLightbox('${sUrl}','${sName}')">
|
|
813
|
+
</div>
|
|
814
|
+
<div class="gcard-body">
|
|
815
|
+
<div class="gcard-name" title="${entry.name||''}">${entry.name||'AI Image'}</div>
|
|
816
|
+
<div class="gcard-actions">
|
|
817
|
+
<button onclick="useLightboxUrlAsRef('${sUrl}','${sName}')">🔧 Use</button>
|
|
818
|
+
<button onclick="copyUrl('${sUrl}')">📋 Copy</button>
|
|
819
|
+
<button onclick="downloadFromUrl('${sUrl}','${sName}.png')">⬇️</button>
|
|
820
|
+
<button class="del" onclick="removeFromGallery('${sUrl}',this.closest('.gcard'))">✕</button>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
`;
|
|
824
|
+
return el;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function loadGallery() {
|
|
828
|
+
try {
|
|
829
|
+
const resp = await fetch('/api/image-gen/saved');
|
|
830
|
+
if (!resp.ok) throw new Error('server error');
|
|
831
|
+
galleryItems = await resp.json();
|
|
832
|
+
renderGallery();
|
|
833
|
+
} catch(e) { console.warn('[Gallery] Load failed:', e.message); }
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function renderGallery() {
|
|
837
|
+
const grid = document.getElementById('gallery-grid');
|
|
838
|
+
const empty = document.getElementById('gallery-empty');
|
|
839
|
+
// Clear existing cards (keep empty placeholder)
|
|
840
|
+
grid.querySelectorAll('.gcard').forEach(c=>c.remove());
|
|
841
|
+
if (galleryItems.length === 0) { empty.style.display='block'; document.getElementById('gallery-count').textContent=''; return; }
|
|
842
|
+
empty.style.display = 'none';
|
|
843
|
+
document.getElementById('gallery-count').textContent = galleryItems.length + ' image' + (galleryItems.length===1?'':'s');
|
|
844
|
+
// Newest first
|
|
845
|
+
[...galleryItems].reverse().forEach(entry => grid.insertBefore(_galleryCard(entry), empty));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function saveToGallery() {
|
|
849
|
+
if (!lastResult) return;
|
|
850
|
+
const entry = { name: lastResult.name, url: lastResult.dataUrl, ts: Date.now() };
|
|
851
|
+
if (lastResult.dataUrl.startsWith('data:')) { toast('⚠️ Image not saved to server yet — generation may have failed', 'err'); return; }
|
|
852
|
+
try {
|
|
853
|
+
await fetch('/api/image-gen/saved', {
|
|
854
|
+
method:'POST', headers:{'Content-Type':'application/json'},
|
|
855
|
+
body: JSON.stringify(entry),
|
|
856
|
+
});
|
|
857
|
+
galleryItems.push(entry);
|
|
858
|
+
renderGallery();
|
|
859
|
+
toast('✅ Saved to gallery!', 'ok');
|
|
860
|
+
// Scroll to gallery
|
|
861
|
+
document.querySelector('.gallery-section').scrollIntoView({behavior:'smooth',block:'start'});
|
|
862
|
+
} catch(e) { toast('❌ Save failed: '+e.message, 'err'); }
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function removeFromGallery(url, cardEl) {
|
|
866
|
+
cardEl?.remove();
|
|
867
|
+
galleryItems = galleryItems.filter(e=>e.url!==url);
|
|
868
|
+
document.getElementById('gallery-count').textContent = galleryItems.length + ' image' + (galleryItems.length===1?'':'s');
|
|
869
|
+
if (galleryItems.length===0) document.getElementById('gallery-empty').style.display='block';
|
|
870
|
+
try {
|
|
871
|
+
await fetch('/api/image-gen/saved', {
|
|
872
|
+
method:'DELETE', headers:{'Content-Type':'application/json'},
|
|
873
|
+
body: JSON.stringify({url}),
|
|
874
|
+
});
|
|
875
|
+
} catch(e) {}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function copyUrl(url) {
|
|
879
|
+
try { await navigator.clipboard.writeText(window.location.origin+url); toast('📋 URL copied!', 'ok'); }
|
|
880
|
+
catch(e) { toast('Copy failed', 'err'); }
|
|
881
|
+
}
|
|
882
|
+
function useLightboxUrlAsRef(url, name) { loadUrlAsRef(url, name); closeLightbox(); }
|
|
883
|
+
|
|
884
|
+
// ── Lightbox ───────────────────────────────────────────────────────
|
|
885
|
+
let _lbUrl='', _lbName='';
|
|
886
|
+
function openLightbox(src, name='') {
|
|
887
|
+
_lbUrl = src; _lbName = name;
|
|
888
|
+
document.getElementById('lightbox-img').src = src;
|
|
889
|
+
document.getElementById('lightbox').classList.add('open');
|
|
890
|
+
}
|
|
891
|
+
function closeLightbox() { document.getElementById('lightbox').classList.remove('open'); }
|
|
892
|
+
function closeLightboxOnBg(e) { if(e.target===document.getElementById('lightbox')) closeLightbox(); }
|
|
893
|
+
|
|
894
|
+
async function copyLightboxUrl() {
|
|
895
|
+
try { await navigator.clipboard.writeText(window.location.origin+_lbUrl); toast('📋 URL copied!','ok'); }
|
|
896
|
+
catch(e) { toast('Copy failed','err'); }
|
|
897
|
+
}
|
|
898
|
+
function downloadLightboxImg() { downloadFromUrl(_lbUrl, (_lbName||'image')+'.png'); }
|
|
899
|
+
function useLightboxAsRef() { loadUrlAsRef(_lbUrl, _lbName); closeLightbox(); }
|
|
900
|
+
|
|
901
|
+
// ── Toast ──────────────────────────────────────────────────────────
|
|
902
|
+
let _toastTimer;
|
|
903
|
+
function toast(msg, type='') {
|
|
904
|
+
const el = document.getElementById('toast');
|
|
905
|
+
el.textContent = msg;
|
|
906
|
+
el.className = 'show' + (type?' '+type:'');
|
|
907
|
+
clearTimeout(_toastTimer);
|
|
908
|
+
_toastTimer = setTimeout(()=>el.className='', 3200);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ── Init ───────────────────────────────────────────────────────────
|
|
912
|
+
loadGallery();
|
|
913
|
+
</script>
|
|
914
|
+
</body>
|
|
915
|
+
</html>
|