shukdev3 commited on
Commit
e36bbcd
Β·
verified Β·
1 Parent(s): 8535260

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +1262 -0
index.html ADDED
@@ -0,0 +1,1262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>ShukGEN v3 β€” Face Style Generator</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
9
+ <style>
10
+ /* ═══════════════════════════════════════════════════════════
11
+ DESIGN TOKENS (mirrors PyQt6 DARK palette)
12
+ ═══════════════════════════════════════════════════════════ */
13
+ :root {
14
+ --bg-dark: #0D0F18;
15
+ --bg-panel: #161927;
16
+ --bg-card: #1E2235;
17
+ --bg-input: #252A3E;
18
+ --accent: #7B5CF0;
19
+ --accent-hover: #9B7FFF;
20
+ --accent2: #38BDF8;
21
+ --success: #34D399;
22
+ --warning: #FBBF24;
23
+ --danger: #F87171;
24
+ --text-primary: #EEF0FF;
25
+ --text-secondary: #8B91B8;
26
+ --text-muted: #4A5070;
27
+ --border: #272C42;
28
+ --sep: #1E2235;
29
+
30
+ --style0: #34D399;
31
+ --style1: #C0392B;
32
+ --style2: #8E44AD;
33
+ --style3: #F59E0B;
34
+ --style4: #EF4444;
35
+ --style5: #F97316;
36
+ --style6: #38BDF8;
37
+ --style7: #14B8A6;
38
+
39
+ --font-ui: 'DM Sans', sans-serif;
40
+ --font-mono: 'Space Mono', monospace;
41
+
42
+ --server: http://127.0.0.1:7860;
43
+ }
44
+
45
+ /* LIGHT THEME */
46
+ body.light {
47
+ --bg-dark: #F0F2FF;
48
+ --bg-panel: #E4E6F5;
49
+ --bg-card: #D8DAF0;
50
+ --bg-input: #C8CAE0;
51
+ --accent: #7B5CF0;
52
+ --accent-hover: #9B7FFF;
53
+ --accent2: #0284C7;
54
+ --success: #059669;
55
+ --warning: #D97706;
56
+ --danger: #DC2626;
57
+ --text-primary: #0D0F18;
58
+ --text-secondary: #3A3D5C;
59
+ --text-muted: #7A7DA0;
60
+ --border: #B0B3D0;
61
+ --sep: #C8CAE0;
62
+ }
63
+
64
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
65
+
66
+ html, body {
67
+ height: 100%;
68
+ background: var(--bg-dark);
69
+ color: var(--text-primary);
70
+ font-family: var(--font-ui);
71
+ font-size: 14px;
72
+ overflow: hidden;
73
+ }
74
+
75
+ /* ── Layout ── */
76
+ #app { display: flex; flex-direction: column; height: 100vh; }
77
+
78
+ /* ── Title Bar ── */
79
+ #titlebar {
80
+ display: flex; align-items: center;
81
+ height: 58px; padding: 0 16px 0 0;
82
+ background: var(--bg-panel);
83
+ border-bottom: 1px solid var(--sep);
84
+ flex-shrink: 0;
85
+ }
86
+ .titlebar-strip {
87
+ width: 5px; height: 58px; flex-shrink: 0;
88
+ background: linear-gradient(to bottom, var(--accent), var(--accent2));
89
+ }
90
+ .titlebar-logo {
91
+ display: flex; align-items: baseline; gap: 2px;
92
+ margin-left: 16px;
93
+ }
94
+ .titlebar-logo .name { font-size: 20px; font-weight: 800; letter-spacing: -1px; }
95
+ .titlebar-logo .ver { font-size: 14px; font-weight: 700; color: var(--accent); padding-top: 2px; }
96
+ .titlebar-sub { color: var(--text-secondary); font-size: 13px; margin-left: 8px; }
97
+ .spacer { flex: 1; }
98
+ .badge {
99
+ padding: 3px 10px; border-radius: 10px;
100
+ font-size: 11px; font-weight: 700; color: #fff;
101
+ background: var(--accent2);
102
+ }
103
+ .badge.gpu { background: var(--success); }
104
+ #theme-btn {
105
+ margin-left: 12px;
106
+ background: var(--bg-input); color: var(--text-secondary);
107
+ border: none; border-radius: 8px; padding: 6px 14px;
108
+ font-size: 12px; font-family: var(--font-ui); cursor: pointer;
109
+ transition: background .2s, color .2s;
110
+ }
111
+ #theme-btn:hover { background: var(--border); color: var(--text-primary); }
112
+
113
+ /* ── Body (sidebar + content) ── */
114
+ #body { display: flex; flex: 1; min-height: 0; }
115
+
116
+ /* ── Sidebar ── */
117
+ #sidebar {
118
+ width: 300px; flex-shrink: 0;
119
+ background: var(--bg-panel);
120
+ border-right: 1px solid var(--sep);
121
+ overflow-y: auto;
122
+ padding: 20px 16px;
123
+ display: flex; flex-direction: column; gap: 4px;
124
+ }
125
+
126
+ /* ── Section Headers ── */
127
+ .section-header {
128
+ display: flex; align-items: center; gap: 10px;
129
+ margin-bottom: 6px;
130
+ }
131
+ .section-header .bar {
132
+ width: 3px; height: 18px; border-radius: 2px;
133
+ background: var(--accent); flex-shrink: 0;
134
+ }
135
+ .section-header span { font-size: 13px; font-weight: 700; }
136
+
137
+ .hs { height: 1px; background: var(--sep); margin: 10px 0; }
138
+
139
+ /* ── Buttons ── */
140
+ .btn {
141
+ display: flex; align-items: center; justify-content: center;
142
+ gap: 6px; width: 100%; padding: 9px 18px;
143
+ font-size: 13px; font-weight: 600; font-family: var(--font-ui);
144
+ border: none; border-radius: 8px; cursor: pointer;
145
+ transition: background .18s, transform .1s, box-shadow .2s;
146
+ letter-spacing: .3px; text-decoration: none;
147
+ }
148
+ .btn:active { transform: scale(.98); }
149
+ .btn-primary { background: var(--accent); color: #fff; }
150
+ .btn-primary:hover { background: var(--accent-hover); box-shadow: 0 0 18px rgba(123,92,240,.35); }
151
+ .btn-success { background: var(--success); color: #fff; }
152
+ .btn-success:hover { background: #52E8A8; }
153
+ .btn-secondary { background: var(--bg-input); color: var(--text-primary); }
154
+ .btn-secondary:hover { background: var(--border); }
155
+ .btn-ghost { background: transparent; color: var(--text-secondary);
156
+ border: 1px solid rgba(123,92,240,.27); }
157
+ .btn-ghost:hover { background: var(--bg-card); }
158
+ .btn-danger { background: var(--danger); color: #fff; }
159
+
160
+ .label-muted { color: var(--text-muted); font-size: 11px; margin-top: 3px; word-break: break-all; }
161
+ .label-status { font-size: 11px; font-weight: 600; }
162
+ .label-status.ok { color: var(--success); }
163
+ .label-status.err { color: var(--danger); }
164
+
165
+ /* Input preview box */
166
+ #input-preview-box {
167
+ width: 100%; height: 190px;
168
+ background: var(--bg-input); border-radius: 8px;
169
+ display: flex; align-items: center; justify-content: center;
170
+ overflow: hidden; margin-top: 8px; position: relative;
171
+ }
172
+ #input-preview-box img {
173
+ width: 100%; height: 100%; object-fit: contain;
174
+ }
175
+ .img-placeholder {
176
+ color: var(--text-muted); font-size: 12px; text-align: center;
177
+ pointer-events: none;
178
+ }
179
+
180
+ /* Progress bar */
181
+ .progress-wrap { height: 6px; background: var(--bg-input); border-radius: 4px; overflow: hidden; margin: 6px 0; }
182
+ .progress-fill {
183
+ height: 100%; width: 0%;
184
+ background: linear-gradient(to right, var(--accent), var(--accent2));
185
+ border-radius: 4px; transition: width .4s ease;
186
+ }
187
+
188
+ /* Quick guide */
189
+ .quick-guide {
190
+ background: var(--bg-input); border-radius: 10px;
191
+ padding: 12px 14px; margin-top: auto;
192
+ }
193
+ .quick-guide .title { color: var(--warning); font-size: 13px; font-weight: 700; margin-bottom: 6px; }
194
+ .quick-guide p { color: var(--text-secondary); font-size: 11px; margin: 2px 0; }
195
+
196
+ /* ── Content Area ── */
197
+ #content { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
198
+
199
+ /* ── Tabs ── */
200
+ .tab-bar {
201
+ display: flex; background: var(--bg-panel);
202
+ border-bottom: 1px solid var(--border);
203
+ flex-shrink: 0;
204
+ }
205
+ .tab {
206
+ padding: 10px 20px; font-size: 12px; font-weight: 500;
207
+ color: var(--text-secondary); border: none; background: none;
208
+ cursor: pointer; font-family: var(--font-ui);
209
+ border-bottom: 2px solid transparent;
210
+ transition: color .15s, border-color .15s, background .15s;
211
+ }
212
+ .tab:hover { background: var(--bg-card); color: var(--text-primary); }
213
+ .tab.active { color: var(--text-primary); border-bottom-color: var(--accent); background: var(--bg-dark); }
214
+
215
+ .tab-content { display: none; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; }
216
+ .tab-content.active { display: flex; }
217
+
218
+ /* Tab header strip */
219
+ .tab-header {
220
+ display: flex; align-items: center; gap: 12px;
221
+ padding: 12px 20px; background: var(--bg-panel);
222
+ border-bottom: 1px solid var(--sep); flex-shrink: 0;
223
+ }
224
+ .tab-header .th-title { font-size: 16px; font-weight: 700; }
225
+ .tab-header .th-sub { font-size: 12px; color: var(--text-muted); }
226
+
227
+ /* ── Style Gallery ── */
228
+ #gallery-scroll { flex: 1; overflow-y: auto; padding: 16px; }
229
+ #gallery-grid {
230
+ display: grid; grid-template-columns: repeat(4, 1fr);
231
+ gap: 12px;
232
+ }
233
+
234
+ /* Style cards */
235
+ .style-card {
236
+ background: var(--bg-card); border-radius: 12px;
237
+ overflow: hidden; display: flex; flex-direction: column;
238
+ transition: transform .2s, box-shadow .2s;
239
+ }
240
+ .style-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,.35); }
241
+ .card-accent-bar { height: 4px; flex-shrink: 0; }
242
+ .card-img-wrap {
243
+ width: 100%; aspect-ratio: 1;
244
+ background: var(--bg-card);
245
+ display: flex; align-items: center; justify-content: center;
246
+ overflow: hidden; cursor: pointer;
247
+ }
248
+ .card-img-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
249
+ .card-img-placeholder {
250
+ color: var(--text-muted); font-size: 11px; text-align: center; padding: 8px;
251
+ }
252
+ .card-meta { padding: 6px 10px 0; }
253
+ .card-style-num { font-size: 11px; font-weight: 700; }
254
+ .card-style-name { font-size: 12px; font-weight: 600; margin-top: 2px; }
255
+ .card-btns {
256
+ display: flex; gap: 6px; padding: 6px 8px 10px;
257
+ }
258
+ .card-btn {
259
+ flex: 1; padding: 4px 8px; font-size: 11px; font-weight: 600;
260
+ border: none; border-radius: 6px; cursor: pointer; font-family: var(--font-ui);
261
+ background: var(--bg-input); color: var(--text-secondary);
262
+ transition: background .15s, color .15s;
263
+ }
264
+ .card-btn:hover { background: var(--border); color: var(--text-primary); }
265
+ .card-btn.save { color: #fff; }
266
+
267
+ /* ── Compare Tab ── */
268
+ #compare-area {
269
+ flex: 1; overflow-y: auto; padding: 20px;
270
+ display: flex; gap: 16px; align-items: flex-start;
271
+ }
272
+ .compare-panel {
273
+ background: var(--bg-card); border-radius: 12px; overflow: hidden;
274
+ flex: 1; max-width: 320px; display: flex; flex-direction: column;
275
+ }
276
+ .compare-panel-header {
277
+ padding: 10px 14px; font-size: 12px; font-weight: 700;
278
+ border-bottom: 3px solid var(--accent);
279
+ }
280
+ .compare-img-wrap {
281
+ width: 100%; aspect-ratio: 1; background: var(--bg-card);
282
+ display: flex; align-items: center; justify-content: center; overflow: hidden;
283
+ }
284
+ .compare-img-wrap img { width: 100%; height: 100%; object-fit: cover; }
285
+ .compare-panel-footer { padding: 8px; }
286
+ .compare-panel-footer button {
287
+ width: 100%; padding: 6px 12px; font-size: 11px;
288
+ background: var(--bg-input); color: var(--text-secondary);
289
+ border: none; border-radius: 6px; cursor: pointer; font-family: var(--font-ui);
290
+ }
291
+ .compare-panel-footer button:hover { background: var(--border); color: var(--text-primary); }
292
+ .compare-select {
293
+ padding: 8px;
294
+ display: flex; align-items: center; gap: 8px;
295
+ }
296
+ .compare-select label { font-size: 12px; color: var(--text-secondary); flex-shrink: 0; }
297
+ .compare-select select {
298
+ flex: 1; padding: 5px 8px;
299
+ background: var(--bg-input); color: var(--text-primary);
300
+ border: 1px solid var(--border); border-radius: 6px;
301
+ font-size: 12px; font-family: var(--font-ui);
302
+ }
303
+
304
+ /* ── Strength Tab ── */
305
+ #strength-area {
306
+ flex: 1; overflow-y: auto; padding: 24px;
307
+ display: flex; gap: 40px; align-items: flex-start; flex-wrap: wrap;
308
+ }
309
+ .strength-controls { display: flex; flex-direction: column; gap: 10px; min-width: 220px; }
310
+ .strength-controls label { font-size: 13px; font-weight: 700; }
311
+ .radio-group { display: flex; flex-direction: column; gap: 6px; }
312
+ .radio-item {
313
+ display: flex; align-items: center; gap: 8px;
314
+ font-size: 12px; color: var(--text-secondary); cursor: pointer;
315
+ }
316
+ .radio-item input[type=radio] { accent-color: var(--accent); width: 16px; height: 16px; }
317
+ .radio-item:has(input:checked) { color: var(--text-primary); }
318
+ .alpha-display {
319
+ font-size: 28px; font-weight: 800; color: var(--accent);
320
+ font-family: var(--font-mono); letter-spacing: -1px;
321
+ }
322
+ input[type=range] {
323
+ width: 280px; accent-color: var(--accent);
324
+ cursor: pointer;
325
+ }
326
+ .preset-row { display: flex; gap: 6px; flex-wrap: wrap; }
327
+ .preset-btn {
328
+ padding: 4px 10px; border-radius: 6px; font-size: 11px;
329
+ background: var(--bg-input); color: var(--text-secondary);
330
+ border: none; cursor: pointer; font-family: var(--font-ui);
331
+ transition: background .15s;
332
+ }
333
+ .preset-btn:hover { background: var(--border); color: var(--text-primary); }
334
+ .strength-previews { display: flex; gap: 12px; flex-wrap: wrap; }
335
+ .strength-preview-card {
336
+ background: var(--bg-card); border-radius: 10px; overflow: hidden;
337
+ display: flex; flex-direction: column; width: 200px;
338
+ }
339
+ .strength-preview-card .card-label {
340
+ padding: 6px 10px; font-size: 11px; font-weight: 700;
341
+ color: var(--text-secondary); background: var(--bg-panel);
342
+ }
343
+ .strength-preview-card img,
344
+ .strength-preview-card .img-ph {
345
+ width: 100%; aspect-ratio: 1; object-fit: cover;
346
+ background: var(--bg-input); display: flex;
347
+ align-items: center; justify-content: center;
348
+ color: var(--text-muted); font-size: 11px; text-align: center;
349
+ }
350
+
351
+ /* ── Latent Walk Tab ── */
352
+ #walk-controls {
353
+ display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
354
+ padding: 16px 20px; background: var(--bg-panel);
355
+ border-bottom: 1px solid var(--sep); flex-shrink: 0;
356
+ }
357
+ #walk-controls label { font-size: 12px; color: var(--text-secondary); }
358
+ #walk-controls select, #walk-controls input[type=number] {
359
+ padding: 5px 10px;
360
+ background: var(--bg-input); color: var(--text-primary);
361
+ border: 1px solid var(--border); border-radius: 6px;
362
+ font-size: 12px; font-family: var(--font-ui);
363
+ }
364
+ #walk-scroll { flex: 1; overflow: auto; padding: 16px; }
365
+ #walk-frames-row { display: flex; gap: 8px; flex-wrap: wrap; }
366
+ .walk-frame-card {
367
+ background: var(--bg-card); border-radius: 8px; overflow: hidden;
368
+ display: flex; flex-direction: column; width: 150px; flex-shrink: 0;
369
+ }
370
+ .walk-frame-card .wf-bar { height: 3px; }
371
+ .walk-frame-card img { width: 150px; height: 150px; object-fit: cover; }
372
+ .walk-frame-card .wf-label {
373
+ text-align: center; font-size: 10px; color: var(--text-muted);
374
+ padding: 4px 0; font-family: var(--font-mono);
375
+ }
376
+
377
+ /* ── Status Bar ── */
378
+ #statusbar {
379
+ height: 32px; padding: 0 16px;
380
+ background: var(--bg-panel); border-top: 1px solid var(--sep);
381
+ display: flex; align-items: center; gap: 12px; flex-shrink: 0;
382
+ font-size: 11px; color: var(--text-muted);
383
+ }
384
+ #status-text { flex: 1; }
385
+ #server-indicator { font-size: 11px; font-weight: 600; }
386
+ #server-indicator.ok { color: var(--success); }
387
+ #server-indicator.err { color: var(--danger); }
388
+
389
+ /* ── Lightbox ── */
390
+ #lightbox {
391
+ display: none; position: fixed; inset: 0;
392
+ background: rgba(0,0,0,.85); z-index: 9999;
393
+ align-items: center; justify-content: center;
394
+ cursor: zoom-out;
395
+ }
396
+ #lightbox.show { display: flex; }
397
+ #lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 8px; object-fit: contain; }
398
+
399
+ /* ── Scrollbars ── */
400
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
401
+ ::-webkit-scrollbar-track { background: var(--bg-panel); }
402
+ ::-webkit-scrollbar-thumb { background: var(--bg-input); border-radius: 4px; }
403
+ ::-webkit-scrollbar-thumb:hover { background: var(--accent); }
404
+
405
+ /* ── Server URL bar ── */
406
+ .server-row {
407
+ display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
408
+ }
409
+ .server-row input {
410
+ flex: 1; padding: 6px 10px;
411
+ background: var(--bg-input); color: var(--text-primary);
412
+ border: 1px solid var(--border); border-radius: 6px;
413
+ font-size: 11px; font-family: var(--font-mono);
414
+ }
415
+ .server-row button {
416
+ padding: 6px 10px; background: var(--accent); color: #fff;
417
+ border: none; border-radius: 6px; font-size: 11px; cursor: pointer;
418
+ }
419
+
420
+ /* ── Model path input ── */
421
+ .path-row {
422
+ display: flex; gap: 6px; align-items: stretch; margin-bottom: 4px;
423
+ }
424
+ .path-row input {
425
+ flex: 1; padding: 7px 10px;
426
+ background: var(--bg-input); color: var(--text-primary);
427
+ border: 1px solid var(--border); border-radius: 8px;
428
+ font-size: 11px; font-family: var(--font-mono);
429
+ min-width: 0;
430
+ }
431
+ .path-row input::placeholder { color: var(--text-muted); }
432
+ .path-row button {
433
+ padding: 7px 12px; background: var(--accent); color: #fff;
434
+ border: none; border-radius: 8px; font-size: 12px; font-weight: 600;
435
+ cursor: pointer; white-space: nowrap;
436
+ }
437
+ .path-row button:hover { background: var(--accent-hover); }
438
+
439
+ /* drag-and-drop zone */
440
+ #drop-zone {
441
+ border: 2px dashed var(--border); border-radius: 8px;
442
+ padding: 16px 10px; text-align: center; cursor: pointer;
443
+ color: var(--text-muted); font-size: 12px;
444
+ transition: border-color .2s, background .2s;
445
+ margin-top: 8px;
446
+ }
447
+ #drop-zone.over { border-color: var(--accent); background: rgba(123,92,240,.08); }
448
+ #drop-zone input { display: none; }
449
+
450
+ @media (max-width: 900px) {
451
+ #gallery-grid { grid-template-columns: repeat(2, 1fr); }
452
+ #sidebar { width: 260px; }
453
+ }
454
+ </style>
455
+ </head>
456
+ <body>
457
+
458
+ <!-- Lightbox -->
459
+ <div id="lightbox" onclick="closeLightbox()">
460
+ <img id="lightbox-img" src="" alt="Preview" />
461
+ </div>
462
+
463
+ <div id="app">
464
+
465
+ <!-- ── Title Bar ── -->
466
+ <div id="titlebar">
467
+ <div class="titlebar-strip"></div>
468
+ <div class="titlebar-logo">
469
+ <span class="name">ShukGEN</span>
470
+ <span class="ver">v3</span>
471
+ </div>
472
+ <span class="titlebar-sub">β€” Face Style Generator</span>
473
+ <div class="spacer"></div>
474
+ <span class="badge" id="device-badge">πŸ–₯ CPU</span>
475
+ <button id="theme-btn" onclick="toggleTheme()">β˜€ Light Mode</button>
476
+ </div>
477
+
478
+ <!-- ── Body ── -->
479
+ <div id="body">
480
+
481
+ <!-- ══ SIDEBAR ══ -->
482
+ <div id="sidebar">
483
+
484
+ <!-- Server -->
485
+ <div class="section-header">
486
+ <div class="bar"></div><span>πŸ”Œ Server</span>
487
+ </div>
488
+ <div class="server-row">
489
+ <input id="server-url-input" type="text" value="http://127.0.0.1:7860" />
490
+ <button onclick="pingServer()">Connect</button>
491
+ </div>
492
+ <div id="server-status" class="label-muted">Not connected β€” start server.py first</div>
493
+
494
+ <div class="hs"></div>
495
+
496
+ <!-- Step 1: Load Model -->
497
+ <div class="section-header">
498
+ <div class="bar"></div><span>β‘  Load Model</span>
499
+ </div>
500
+ <div class="path-row">
501
+ <input id="model-path-input" type="text" placeholder="Paste full path to .pth file…" />
502
+ <button onclick="loadModel()">Load</button>
503
+ </div>
504
+ <div id="model-path-label" class="label-muted">No model loaded</div>
505
+ <div id="model-badge" class="label-status err">● Not Loaded</div>
506
+
507
+ <div class="hs"></div>
508
+
509
+ <!-- Step 2: Upload Image -->
510
+ <div class="section-header">
511
+ <div class="bar"></div><span>β‘‘ Upload Face Image</span>
512
+ </div>
513
+ <div id="drop-zone" onclick="document.getElementById('file-input').click()"
514
+ ondragover="event.preventDefault(); this.classList.add('over')"
515
+ ondragleave="this.classList.remove('over')"
516
+ ondrop="handleDrop(event)">
517
+ <input type="file" id="file-input" accept="image/*" onchange="handleFileSelect(event)" />
518
+ πŸ–Ό Click or drag & drop a face image
519
+ </div>
520
+ <div id="img-path-label" class="label-muted" style="margin-top:4px;">No image selected</div>
521
+ <div id="input-preview-box">
522
+ <span class="img-placeholder">Upload a face image</span>
523
+ </div>
524
+
525
+ <div class="hs"></div>
526
+
527
+ <!-- Step 3: Generate -->
528
+ <div class="section-header">
529
+ <div class="bar"></div><span>β‘’ Generate Styles</span>
530
+ </div>
531
+ <button class="btn btn-success" onclick="generateAll()">✨ Generate All 8 Styles</button>
532
+ <div class="progress-wrap" style="margin-top:6px;">
533
+ <div class="progress-fill" id="progress-fill"></div>
534
+ </div>
535
+ <div id="gen-status" class="label-muted">Ready</div>
536
+
537
+ <div class="hs"></div>
538
+
539
+ <!-- Step 4: Export -->
540
+ <div class="section-header">
541
+ <div class="bar"></div><span>β‘£ Export & Tools</span>
542
+ </div>
543
+ <button class="btn btn-secondary" onclick="saveAll()" style="margin-bottom:4px;">πŸ’Ύ Save All Outputs</button>
544
+ <button class="btn btn-ghost" onclick="showAbout()">ℹ️ Architecture Info</button>
545
+
546
+ <!-- Quick Guide -->
547
+ <div class="quick-guide" style="margin-top:16px;">
548
+ <div class="title">πŸ’‘ Quick Guide</div>
549
+ <p>1. Start server.py, then Connect</p>
550
+ <p>2. Paste .pth model path, click Load</p>
551
+ <p>3. Upload a face photo</p>
552
+ <p>4. Click Generate All Styles</p>
553
+ <p>5. Preview / Save any result</p>
554
+ <p>6. Try Strength &amp; Walk tabs!</p>
555
+ </div>
556
+
557
+ </div><!-- /sidebar -->
558
+
559
+ <!-- ══ CONTENT ══ -->
560
+ <div id="content">
561
+
562
+ <!-- Tab bar -->
563
+ <div class="tab-bar">
564
+ <button class="tab active" onclick="switchTab('gallery', this)">🎨 Style Gallery</button>
565
+ <button class="tab" onclick="switchTab('compare', this)">πŸ” Compare</button>
566
+ <button class="tab" onclick="switchTab('strength', this)">🎚 Strength</button>
567
+ <button class="tab" onclick="switchTab('walk', this)">🚢 Latent Walk</button>
568
+ </div>
569
+
570
+ <!-- ── Tab: Gallery ── -->
571
+ <div class="tab-content active" id="tab-gallery">
572
+ <div class="tab-header">
573
+ <span class="th-title">8 Visually Distinct Style Variants</span>
574
+ <span class="th-sub">Each style applies different pixel transforms β€” mathematically guaranteed to look distinct</span>
575
+ </div>
576
+ <div id="gallery-scroll">
577
+ <div id="gallery-grid">
578
+ <!-- Cards built by JS -->
579
+ </div>
580
+ </div>
581
+ </div>
582
+
583
+ <!-- ── Tab: Compare ── -->
584
+ <div class="tab-content" id="tab-compare">
585
+ <div class="tab-header">
586
+ <span class="th-title">Original vs Reconstruction vs Styled</span>
587
+ </div>
588
+ <div id="compare-area">
589
+ <!-- Original -->
590
+ <div class="compare-panel">
591
+ <div class="compare-panel-header" style="border-color: var(--accent2);">Original Input</div>
592
+ <div class="compare-img-wrap" id="cmp-orig-wrap">
593
+ <span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Upload image first</span>
594
+ </div>
595
+ <div class="compare-panel-footer">
596
+ <button onclick="openLightbox('orig')">πŸ‘ Full Size Preview</button>
597
+ </div>
598
+ </div>
599
+ <!-- Reconstruction -->
600
+ <div class="compare-panel">
601
+ <div class="compare-panel-header" style="border-color: var(--success);">VAE Reconstruction</div>
602
+ <div class="compare-img-wrap" id="cmp-recon-wrap">
603
+ <span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Generate first</span>
604
+ </div>
605
+ <div class="compare-panel-footer">
606
+ <button onclick="openLightbox('recon')">πŸ‘ Full Size Preview</button>
607
+ </div>
608
+ </div>
609
+ <!-- Styled -->
610
+ <div class="compare-panel">
611
+ <div class="compare-panel-header" style="border-color: var(--accent);">Styled Version</div>
612
+ <div class="compare-img-wrap" id="cmp-styled-wrap">
613
+ <span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Generate first</span>
614
+ </div>
615
+ <div class="compare-select">
616
+ <label>Style:</label>
617
+ <select id="compare-style-sel" onchange="updateCompareStyled()"></select>
618
+ </div>
619
+ <div class="compare-panel-footer">
620
+ <button onclick="openLightbox('styled')">πŸ‘ Full Size Preview</button>
621
+ </div>
622
+ </div>
623
+ </div>
624
+ </div>
625
+
626
+ <!-- ── Tab: Strength ── -->
627
+ <div class="tab-content" id="tab-strength">
628
+ <div class="tab-header">
629
+ <span class="th-title">Style Strength Control (Ξ±-Blending)</span>
630
+ <span class="th-sub">Ξ±=0 β†’ pure reconstruction &nbsp;|&nbsp; Ξ±=1 β†’ full style effect</span>
631
+ </div>
632
+ <div id="strength-area">
633
+ <div class="strength-controls">
634
+ <label>Select Style:</label>
635
+ <div class="radio-group" id="strength-radio-group">
636
+ <!-- built by JS -->
637
+ </div>
638
+ <div style="margin-top:16px;">
639
+ <label>Blend Strength (Ξ±):</label>
640
+ <div class="alpha-display" id="alpha-display">Ξ± = 1.00</div>
641
+ <input type="range" id="alpha-slider" min="0" max="100" value="100"
642
+ oninput="onAlphaChange(this.value)" style="margin: 8px 0;" />
643
+ <div class="preset-row">
644
+ <button class="preset-btn" onclick="setAlpha(0)">0%</button>
645
+ <button class="preset-btn" onclick="setAlpha(25)">25%</button>
646
+ <button class="preset-btn" onclick="setAlpha(50)">50%</button>
647
+ <button class="preset-btn" onclick="setAlpha(75)">75%</button>
648
+ <button class="preset-btn" onclick="setAlpha(100)">100%</button>
649
+ </div>
650
+ </div>
651
+ <button class="btn btn-primary" onclick="updateStrengthPreview()" style="margin-top:12px;">
652
+ πŸ”„ Update Preview
653
+ </button>
654
+ </div>
655
+ <div class="strength-previews">
656
+ <div class="strength-preview-card">
657
+ <div class="card-label">Reconstruction</div>
658
+ <div id="sp-recon" class="img-ph" style="display:flex;align-items:center;justify-content:center;min-height:200px;">Generate first</div>
659
+ </div>
660
+ <div class="strength-preview-card">
661
+ <div class="card-label">Blended (Ξ±)</div>
662
+ <div id="sp-blend" class="img-ph" style="display:flex;align-items:center;justify-content:center;min-height:200px;">Generate first</div>
663
+ </div>
664
+ <div class="strength-preview-card">
665
+ <div class="card-label">Full Style</div>
666
+ <div id="sp-styled" class="img-ph" style="display:flex;align-items:center;justify-content:center;min-height:200px;">Generate first</div>
667
+ </div>
668
+ </div>
669
+ </div>
670
+ </div>
671
+
672
+ <!-- ── Tab: Latent Walk ── -->
673
+ <div class="tab-content" id="tab-walk">
674
+ <div class="tab-header">
675
+ <span class="th-title">Latent Space Walk</span>
676
+ <span class="th-sub">Traverse the VAE latent space between two style directions</span>
677
+ </div>
678
+ <div id="walk-controls">
679
+ <label>Style A:
680
+ <select id="walk-style-a"></select>
681
+ </label>
682
+ <label>Style B:
683
+ <select id="walk-style-b"></select>
684
+ </label>
685
+ <label>Steps:
686
+ <input type="number" id="walk-steps" value="7" min="2" max="16" style="width:60px;" />
687
+ </label>
688
+ <button class="btn btn-primary" style="width:auto;padding:8px 20px;" onclick="runWalk()">
689
+ 🚢 Run Walk
690
+ </button>
691
+ <span id="walk-status" style="font-size:11px;color:var(--text-muted);">Generate styles first</span>
692
+ </div>
693
+ <div id="walk-scroll">
694
+ <div id="walk-frames-row">
695
+ <span style="color:var(--text-muted);font-size:12px;">Run a latent walk to see frames here.</span>
696
+ </div>
697
+ </div>
698
+ </div>
699
+
700
+ </div><!-- /content -->
701
+ </div><!-- /body -->
702
+
703
+ <!-- ── Status Bar ── -->
704
+ <div id="statusbar">
705
+ <span id="status-text">Ready β€” start server.py and click Connect</span>
706
+ <span id="server-indicator" class="err">● Disconnected</span>
707
+ </div>
708
+
709
+ </div><!-- /app -->
710
+
711
+ <!-- About dialog -->
712
+ <div id="about-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:8888;align-items:center;justify-content:center;">
713
+ <div style="background:var(--bg-dark);border-radius:12px;max-width:560px;width:90%;max-height:80vh;overflow-y:auto;padding:24px;">
714
+ <h2 style="font-size:18px;font-weight:800;margin-bottom:8px;">ShukGEN v3 β€” Architecture Info</h2>
715
+ <pre id="about-text" style="font-family:var(--font-mono);font-size:11px;color:var(--text-secondary);white-space:pre-wrap;line-height:1.6;"></pre>
716
+ <button class="btn btn-secondary" style="margin-top:16px;" onclick="document.getElementById('about-overlay').style.display='none'">Close</button>
717
+ </div>
718
+ </div>
719
+
720
+ <script>
721
+ // ═══════════════════════════════════════════════════════════
722
+ // CONSTANTS
723
+ // ═══════════════════════════════════════════════════════════
724
+ const STYLE_NAMES = [
725
+ "Youthful", "Aged / Mature", "Dramatic Light", "Soft Glow",
726
+ "Intense / Bold", "Warm Golden Hour", "Cool / Moody", "Sketch / Artistic",
727
+ ];
728
+ const STYLE_ICONS = ["🌸","🌿","⚑","✨","πŸ”₯","πŸŒ…","🌊","🎨"];
729
+ const STYLE_COLORS = ["#34D399","#C0392B","#8E44AD","#F59E0B","#EF4444","#F97316","#38BDF8","#14B8A6"];
730
+
731
+ // ═══════════════════════════════════════════════════════════
732
+ // STATE
733
+ // ═══════════════════════════════════════════════════════════
734
+ const S = {
735
+ serverURL: 'http://127.0.0.1:7860',
736
+ modelLoaded: false,
737
+ imageLoaded: false,
738
+ generated: false,
739
+ origB64: null,
740
+ reconB64: null,
741
+ styledB64: new Array(8).fill(null),
742
+ uploadedImg: null, // File object
743
+ };
744
+
745
+ // ═══════════════════════════════════════════════════════════
746
+ // SERVER HELPERS
747
+ // ═══════════════════════════════════════════════════════════
748
+ function getURL() {
749
+ return document.getElementById('server-url-input').value.replace(/\/$/, '');
750
+ }
751
+
752
+ async function apiFetch(path, opts = {}) {
753
+ const url = getURL() + path;
754
+ const res = await fetch(url, {
755
+ headers: { 'Content-Type': 'application/json' },
756
+ ...opts,
757
+ });
758
+ return res.json();
759
+ }
760
+
761
+ // ═════════════════════���═════════════════════════════════════
762
+ // PING / CONNECT
763
+ // ═══════════════════════════════════════════════════════════
764
+ async function pingServer() {
765
+ setStatus('Connecting to server…');
766
+ try {
767
+ const data = await apiFetch('/status');
768
+ document.getElementById('server-status').textContent = 'βœ… Connected';
769
+ document.getElementById('server-indicator').textContent = '● Connected';
770
+ document.getElementById('server-indicator').className = 'ok';
771
+
772
+ // Device badge
773
+ const badge = document.getElementById('device-badge');
774
+ if (data.device && data.device.startsWith('GPU')) {
775
+ badge.textContent = '⚑ ' + data.device;
776
+ badge.classList.add('gpu');
777
+ } else {
778
+ badge.textContent = 'πŸ–₯ ' + (data.device || 'CPU');
779
+ }
780
+
781
+ setStatus(`Connected β€” PyTorch ${data.torch_version} | ${data.device}`);
782
+
783
+ if (data.model_loaded) {
784
+ S.modelLoaded = true;
785
+ document.getElementById('model-badge').textContent = '● Loaded';
786
+ document.getElementById('model-badge').className = 'label-status ok';
787
+ const cfg = data.model_config;
788
+ document.getElementById('model-path-label').textContent =
789
+ `latent_dim=${cfg.latent_dim} | base_filters=${cfg.base_filters} | img_size=${data.image_size || '?'}`;
790
+ }
791
+ } catch (e) {
792
+ document.getElementById('server-status').textContent = '❌ Cannot reach server β€” is server.py running?';
793
+ document.getElementById('server-indicator').textContent = '● Disconnected';
794
+ document.getElementById('server-indicator').className = 'err';
795
+ setStatus('Cannot reach server. Start server.py first.');
796
+ }
797
+ }
798
+
799
+ // ═══════════════════════════════════════════════════════════
800
+ // LOAD MODEL
801
+ // ═══════════════════════════════════════════════════════════
802
+ async function loadModel() {
803
+ const path = document.getElementById('model-path-input').value.trim();
804
+ if (!path) { alert('Please paste the full path to your .pth file.'); return; }
805
+ setStatus('⏳ Loading model…');
806
+ try {
807
+ const data = await apiFetch('/load_model', {
808
+ method: 'POST',
809
+ body: JSON.stringify({ path }),
810
+ });
811
+ if (data.error) {
812
+ setStatus('❌ ' + data.error);
813
+ document.getElementById('model-badge').textContent = '● Load Failed';
814
+ document.getElementById('model-badge').className = 'label-status err';
815
+ alert('Model load error:\n' + data.error);
816
+ return;
817
+ }
818
+ S.modelLoaded = true;
819
+ document.getElementById('model-badge').textContent = '● Loaded βœ“';
820
+ document.getElementById('model-badge').className = 'label-status ok';
821
+ document.getElementById('model-path-label').textContent =
822
+ `${data.filename} | latent=${data.config.latent_dim} base_f=${data.config.base_filters} img=${data.image_size}`;
823
+ setStatus(`βœ… Model loaded: ${data.filename}`);
824
+ } catch (e) {
825
+ setStatus('❌ Network error β€” is server running?');
826
+ }
827
+ }
828
+
829
+ // ═══════════════════════════════════════════════════════════
830
+ // IMAGE UPLOAD
831
+ // ═══════════════════════════════════════════════════════════
832
+ function handleDrop(e) {
833
+ e.preventDefault();
834
+ document.getElementById('drop-zone').classList.remove('over');
835
+ const file = e.dataTransfer.files[0];
836
+ if (file && file.type.startsWith('image/')) loadImageFile(file);
837
+ }
838
+ function handleFileSelect(e) {
839
+ const file = e.target.files[0];
840
+ if (file) loadImageFile(file);
841
+ }
842
+
843
+ function loadImageFile(file) {
844
+ const reader = new FileReader();
845
+ reader.onload = async (ev) => {
846
+ const b64 = ev.target.result;
847
+ S.origB64 = b64;
848
+ S.imageLoaded = true;
849
+ document.getElementById('img-path-label').textContent = file.name;
850
+
851
+ // Show preview in sidebar
852
+ const box = document.getElementById('input-preview-box');
853
+ box.innerHTML = `<img src="${b64}" style="width:100%;height:100%;object-fit:contain;" />`;
854
+
855
+ // Update compare original
856
+ updateCompareOrig(b64);
857
+
858
+ setStatus('πŸ–Ό Image loaded: ' + file.name);
859
+
860
+ // Send to server
861
+ try {
862
+ const data = await apiFetch('/load_image', {
863
+ method: 'POST',
864
+ body: JSON.stringify({ image_b64: b64 }),
865
+ });
866
+ if (data.error) setStatus('❌ Image upload error: ' + data.error);
867
+ } catch (e) {
868
+ setStatus('⚠ Could not send image to server β€” check connection.');
869
+ }
870
+ };
871
+ reader.readAsDataURL(file);
872
+ }
873
+
874
+ // ═══════════════════════════════════════════════════════════
875
+ // GENERATE
876
+ // ═══════════════════════════════════════════════════════════
877
+ async function generateAll() {
878
+ if (!S.modelLoaded) { alert('Load a model first.'); return; }
879
+ if (!S.imageLoaded) { alert('Upload an image first.'); return; }
880
+
881
+ setProgress(0);
882
+ setGenStatus('⏳ Encoding face with VAE…');
883
+ setStatus('⏳ Encoding face with VAE…');
884
+
885
+ // Set all cards to loading
886
+ for (let i = 0; i < 8; i++) setCardLoading(i);
887
+
888
+ try {
889
+ const data = await apiFetch('/generate', { method: 'POST', body: '{}' });
890
+ if (data.error) {
891
+ setGenStatus('❌ Error');
892
+ setStatus('❌ Generate error: ' + data.error);
893
+ alert('Generation error:\n' + data.error);
894
+ return;
895
+ }
896
+
897
+ S.reconB64 = data.recon;
898
+ S.styledB64 = data.styles;
899
+ S.generated = true;
900
+
901
+ setProgress(100);
902
+
903
+ // Update gallery cards
904
+ for (let i = 0; i < 8; i++) setCardImage(i, data.styles[i]);
905
+
906
+ // Update compare tab
907
+ updateCompareRecon(data.recon);
908
+ updateCompareStyled();
909
+
910
+ // Update strength previews (recon panel)
911
+ setImgOrPh('sp-recon', data.recon);
912
+
913
+ setGenStatus('βœ… Done β€” 8 styles generated!');
914
+ setStatus('βœ… All 8 styles generated successfully.');
915
+ } catch (e) {
916
+ setGenStatus('❌ Network error');
917
+ setStatus('❌ Generation failed β€” is server running?');
918
+ }
919
+ }
920
+
921
+ // ═══════════════════════════════════════════════════════════
922
+ // STRENGTH PREVIEW
923
+ // ═══════════════════════════════════════════════════════════
924
+ function onAlphaChange(val) {
925
+ const v = (val / 100).toFixed(2);
926
+ document.getElementById('alpha-display').textContent = `Ξ± = ${v}`;
927
+ }
928
+ function setAlpha(val) {
929
+ document.getElementById('alpha-slider').value = val;
930
+ onAlphaChange(val);
931
+ }
932
+
933
+ async function updateStrengthPreview() {
934
+ if (!S.generated) { alert('Generate styles first.'); return; }
935
+ const styleIdx = getCheckedRadio();
936
+ const alpha = document.getElementById('alpha-slider').value / 100;
937
+
938
+ // Recon always shown
939
+ setImgOrPh('sp-recon', S.reconB64);
940
+ // Full styled
941
+ setImgOrPh('sp-styled', S.styledB64[styleIdx]);
942
+
943
+ // Blend from server
944
+ try {
945
+ const data = await apiFetch('/strength_preview', {
946
+ method: 'POST',
947
+ body: JSON.stringify({ style_idx: styleIdx, alpha }),
948
+ });
949
+ if (data.blend) setImgOrPh('sp-blend', data.blend);
950
+ } catch (e) {
951
+ setStatus('❌ Strength preview failed');
952
+ }
953
+ }
954
+
955
+ function getCheckedRadio() {
956
+ const radios = document.querySelectorAll('#strength-radio-group input[type=radio]');
957
+ for (let r of radios) if (r.checked) return parseInt(r.value);
958
+ return 0;
959
+ }
960
+
961
+ // ═══════════════════════════════════════════════════════════
962
+ // LATENT WALK
963
+ // ═══════════════════════════════════════════════════════════
964
+ async function runWalk() {
965
+ if (!S.generated) { alert('Generate styles first.'); return; }
966
+ const styleA = parseInt(document.getElementById('walk-style-a').value);
967
+ const styleB = parseInt(document.getElementById('walk-style-b').value);
968
+ const steps = parseInt(document.getElementById('walk-steps').value);
969
+ document.getElementById('walk-status').textContent = '⏳ Running…';
970
+ document.getElementById('walk-frames-row').innerHTML =
971
+ '<span style="color:var(--text-muted);font-size:12px;">Generating frames…</span>';
972
+
973
+ try {
974
+ const data = await apiFetch('/latent_walk', {
975
+ method: 'POST',
976
+ body: JSON.stringify({ style_a: styleA, style_b: styleB, steps }),
977
+ });
978
+ if (data.error) {
979
+ document.getElementById('walk-status').textContent = '❌ ' + data.error;
980
+ return;
981
+ }
982
+ const row = document.getElementById('walk-frames-row');
983
+ row.innerHTML = '';
984
+ data.frames.forEach((f, i) => {
985
+ const color = f.alpha < 0.5 ? STYLE_COLORS[styleA] : STYLE_COLORS[styleB];
986
+ const card = document.createElement('div');
987
+ card.className = 'walk-frame-card';
988
+ card.innerHTML = `
989
+ <div class="wf-bar" style="background:${color};"></div>
990
+ <img src="${f.img}" loading="lazy" onclick="openLightboxSrc('${f.img}')" style="cursor:zoom-in;" />
991
+ <div class="wf-label">Ξ±=${f.alpha.toFixed(2)}</div>
992
+ `;
993
+ row.appendChild(card);
994
+ });
995
+ document.getElementById('walk-status').textContent =
996
+ `βœ… ${data.frames.length} frames: ${STYLE_NAMES[styleA]} β†’ ${STYLE_NAMES[styleB]}`;
997
+ setStatus(`βœ… Latent walk: ${data.frames.length} frames`);
998
+ } catch (e) {
999
+ document.getElementById('walk-status').textContent = '❌ Network error';
1000
+ setStatus('❌ Walk failed β€” is server running?');
1001
+ }
1002
+ }
1003
+
1004
+ // ═══════════════════════════════════════════════════════════
1005
+ // COMPARE TAB
1006
+ // ═══════════════════════════════════════════════════════════
1007
+ function updateCompareOrig(b64) {
1008
+ setImgInWrap('cmp-orig-wrap', b64, 'Original');
1009
+ }
1010
+ function updateCompareRecon(b64) {
1011
+ setImgInWrap('cmp-recon-wrap', b64, 'Reconstruction');
1012
+ }
1013
+ function updateCompareStyled() {
1014
+ const idx = parseInt(document.getElementById('compare-style-sel').value);
1015
+ const b64 = S.styledB64[idx];
1016
+ if (b64) setImgInWrap('cmp-styled-wrap', b64, STYLE_NAMES[idx]);
1017
+ else {
1018
+ document.getElementById('cmp-styled-wrap').innerHTML =
1019
+ `<span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Generate first</span>`;
1020
+ }
1021
+ }
1022
+
1023
+ function setImgInWrap(id, b64, label) {
1024
+ const wrap = document.getElementById(id);
1025
+ wrap.innerHTML = `<img src="${b64}" style="width:100%;height:100%;object-fit:cover;" alt="${label}" onclick="openLightboxSrc('${b64}')" style="cursor:zoom-in;" />`;
1026
+ }
1027
+
1028
+ // ═══════════════════════════════════════════════════════════
1029
+ // SAVE
1030
+ // ═══════════════════════════════════════════════════════════
1031
+ function downloadB64(b64, filename) {
1032
+ const a = document.createElement('a');
1033
+ a.href = b64;
1034
+ a.download = filename;
1035
+ a.click();
1036
+ }
1037
+
1038
+ function saveStyle(idx) {
1039
+ const b64 = S.styledB64[idx];
1040
+ if (!b64) { alert('Generate styles first.'); return; }
1041
+ const name = STYLE_NAMES[idx].replace(/[/ ]+/g, '_').toLowerCase();
1042
+ downloadB64(b64, `shukgen_v3_style_${idx}_${name}.png`);
1043
+ setStatus(`πŸ’Ύ Saved style ${idx}: ${STYLE_NAMES[idx]}`);
1044
+ }
1045
+
1046
+ async function saveAll() {
1047
+ if (!S.generated && !S.imageLoaded) { alert('Generate styles first.'); return; }
1048
+ if (S.origB64) downloadB64(S.origB64, '00_original.png');
1049
+ if (S.reconB64) downloadB64(S.reconB64, '01_reconstruction.png');
1050
+ S.styledB64.forEach((b64, i) => {
1051
+ if (b64) {
1052
+ const name = STYLE_NAMES[i].replace(/[/ ]+/g, '_').toLowerCase();
1053
+ setTimeout(() => downloadB64(b64, `style_${String(i).padStart(2,'0')}_${name}.png`), i * 100);
1054
+ }
1055
+ });
1056
+ setStatus('πŸ’Ύ Saving all generated images…');
1057
+ }
1058
+
1059
+ // ═══════════════════════════════════════════════════════════
1060
+ // LIGHTBOX
1061
+ // ═══════════════════════════════════════════════════════════
1062
+ const _lightboxSrcs = {};
1063
+ function openLightbox(key) {
1064
+ const srcs = { orig: S.origB64, recon: S.reconB64, styled: S.styledB64[parseInt(document.getElementById('compare-style-sel').value)] };
1065
+ const src = srcs[key];
1066
+ if (!src) return;
1067
+ openLightboxSrc(src);
1068
+ }
1069
+ function openLightboxSrc(src) {
1070
+ document.getElementById('lightbox-img').src = src;
1071
+ document.getElementById('lightbox').classList.add('show');
1072
+ }
1073
+ function closeLightbox() {
1074
+ document.getElementById('lightbox').classList.remove('show');
1075
+ }
1076
+
1077
+ // ═══════════════════════════════════════════════════════════
1078
+ // GALLERY CARDS
1079
+ // ═══════════════════════════════════════════════════════════
1080
+ function buildGallery() {
1081
+ const grid = document.getElementById('gallery-grid');
1082
+ grid.innerHTML = '';
1083
+ for (let i = 0; i < 8; i++) {
1084
+ const color = STYLE_COLORS[i];
1085
+ const card = document.createElement('div');
1086
+ card.className = 'style-card';
1087
+ card.id = `card-${i}`;
1088
+ card.innerHTML = `
1089
+ <div class="card-accent-bar" style="background:${color};"></div>
1090
+ <div class="card-img-wrap" id="card-img-wrap-${i}" onclick="previewStyle(${i})">
1091
+ <span class="card-img-placeholder">Pending…</span>
1092
+ </div>
1093
+ <div class="card-meta">
1094
+ <div class="card-style-num" style="color:${color};">${STYLE_ICONS[i]} Style ${i}</div>
1095
+ <div class="card-style-name">${STYLE_NAMES[i]}</div>
1096
+ </div>
1097
+ <div class="card-btns">
1098
+ <button class="card-btn" onclick="previewStyle(${i})">πŸ‘ Preview</button>
1099
+ <button class="card-btn save" style="background:${color};" onclick="saveStyle(${i})">πŸ’Ύ Save</button>
1100
+ </div>
1101
+ `;
1102
+ grid.appendChild(card);
1103
+ }
1104
+ }
1105
+
1106
+ function setCardLoading(i) {
1107
+ document.getElementById(`card-img-wrap-${i}`).innerHTML =
1108
+ '<span class="card-img-placeholder">Generating…</span>';
1109
+ }
1110
+ function setCardImage(i, b64) {
1111
+ document.getElementById(`card-img-wrap-${i}`).innerHTML =
1112
+ `<img src="${b64}" alt="${STYLE_NAMES[i]}" />`;
1113
+ }
1114
+ function previewStyle(i) {
1115
+ if (!S.styledB64[i]) { alert('Generate styles first.'); return; }
1116
+ openLightboxSrc(S.styledB64[i]);
1117
+ }
1118
+
1119
+ // ═══════════════════════════════════════════════════════════
1120
+ // COMPARE STYLE SELECT
1121
+ // ═══════════════════════════════════════════════════════════
1122
+ function buildCompareSelect() {
1123
+ const sel = document.getElementById('compare-style-sel');
1124
+ sel.innerHTML = '';
1125
+ STYLE_NAMES.forEach((n, i) => {
1126
+ const opt = document.createElement('option');
1127
+ opt.value = i; opt.textContent = `${i}: ${n}`;
1128
+ sel.appendChild(opt);
1129
+ });
1130
+ }
1131
+
1132
+ // ═══════════════════════════════════════════════════════════
1133
+ // STRENGTH RADIO GROUP
1134
+ // ═══════════════════════════════════════════════════════════
1135
+ function buildStrengthRadios() {
1136
+ const group = document.getElementById('strength-radio-group');
1137
+ group.innerHTML = '';
1138
+ STYLE_NAMES.forEach((n, i) => {
1139
+ const lbl = document.createElement('label');
1140
+ lbl.className = 'radio-item';
1141
+ lbl.innerHTML = `
1142
+ <input type="radio" name="strength-style" value="${i}" ${i===0?'checked':''} />
1143
+ ${STYLE_ICONS[i]} ${n}
1144
+ `;
1145
+ group.appendChild(lbl);
1146
+ });
1147
+ }
1148
+
1149
+ // ═══════════════════════════════════════════════════════════
1150
+ // WALK SELECTS
1151
+ // ═══════════════════════════════════════════════════════════
1152
+ function buildWalkSelects() {
1153
+ ['walk-style-a','walk-style-b'].forEach((id, j) => {
1154
+ const sel = document.getElementById(id);
1155
+ sel.innerHTML = '';
1156
+ STYLE_NAMES.forEach((n, i) => {
1157
+ const opt = document.createElement('option');
1158
+ opt.value = i; opt.textContent = `${i}: ${n}`;
1159
+ if (j === 0 && i === 0) opt.selected = true;
1160
+ if (j === 1 && i === 1) opt.selected = true;
1161
+ sel.appendChild(opt);
1162
+ });
1163
+ });
1164
+ }
1165
+
1166
+ // ═══════════════════════════════════════════════════════════
1167
+ // ABOUT
1168
+ // ═══════════════════════════════════════════════════════════
1169
+ function showAbout() {
1170
+ const overlay = document.getElementById('about-overlay');
1171
+ overlay.style.display = 'flex';
1172
+ fetch(getURL() + '/status').then(r => r.json()).then(data => {
1173
+ const cfg = data.model_config || {};
1174
+ document.getElementById('about-text').textContent = [
1175
+ 'ShukGEN v3 β€” Face Style Generator',
1176
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
1177
+ '',
1178
+ 'MODEL ARCHITECTURE: FaceVAE',
1179
+ ` Latent Dimension : ${cfg.latent_dim ?? 'β€”'}`,
1180
+ ` Base Filters : ${cfg.base_filters ?? 'β€”'}`,
1181
+ ` Image Size : ${data.image_size ?? 'β€”'}`,
1182
+ ` Device : ${data.device ?? 'β€”'}`,
1183
+ ` PyTorch : ${data.torch_version ?? 'β€”'}`,
1184
+ '',
1185
+ 'ENCODER',
1186
+ ' Inc β†’ DownBlockΓ—4 β†’ Bottleneck β†’ Pool β†’ fc_mu / fc_logvar',
1187
+ ' Bottleneck: ResBlock β†’ AttnBlock β†’ ResBlock β†’ SE',
1188
+ '',
1189
+ 'DECODER',
1190
+ ' fc_dec β†’ Upsample β†’ UpBlockΓ—4 β†’ outc (Tanh)',
1191
+ '',
1192
+ 'STYLE TRANSFORMS (8 built-in, pixel-space):',
1193
+ ' 0: Youthful β€” saturation boost + subtle smoothing',
1194
+ ' 1: Aged / Mature β€” desaturation + warm tint + grain + vignette',
1195
+ ' 2: Dramatic Light β€” horizontal gradient + high contrast + vignette',
1196
+ ' 3: Soft Glow β€” blur additive blend + warm tint + lift',
1197
+ ' 4: Intense / Bold β€” heavy saturation + contrast + sharpness',
1198
+ ' 5: Warm Golden Hr β€” red/green boost + blue reduction',
1199
+ ' 6: Cool / Moody β€” red reduction + blue boost + grain + vignette',
1200
+ ' 7: Sketch/Artisticβ€” posterize + edge detection + canvas grain',
1201
+ '',
1202
+ 'LATENT WALK',
1203
+ ' Random direction sampled in latent space.',
1204
+ ' Walk range: βˆ’2.5Οƒ to +2.5Οƒ, styles blended by Ξ±.',
1205
+ ].join('\n');
1206
+ }).catch(() => {
1207
+ document.getElementById('about-text').textContent = 'Connect to server to see architecture details.';
1208
+ });
1209
+ }
1210
+
1211
+ // ═══════════════════════════════════════════════════════════
1212
+ // HELPERS
1213
+ // ═══════════════════════════════════════════════════════════
1214
+ function setStatus(msg) {
1215
+ document.getElementById('status-text').textContent = msg;
1216
+ }
1217
+ function setGenStatus(msg) {
1218
+ document.getElementById('gen-status').textContent = msg;
1219
+ }
1220
+ function setProgress(pct) {
1221
+ document.getElementById('progress-fill').style.width = pct + '%';
1222
+ }
1223
+ function setImgOrPh(id, b64) {
1224
+ const el = document.getElementById(id);
1225
+ if (b64) {
1226
+ el.outerHTML = `<img id="${id}" src="${b64}" style="width:100%;aspect-ratio:1;object-fit:cover;cursor:zoom-in;" onclick="openLightboxSrc('${b64}')" />`;
1227
+ }
1228
+ }
1229
+
1230
+ // ── Tab switching ──
1231
+ function switchTab(name, btn) {
1232
+ document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
1233
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1234
+ document.getElementById('tab-' + name).classList.add('active');
1235
+ btn.classList.add('active');
1236
+ }
1237
+
1238
+ // ── Theme ──
1239
+ let isDark = true;
1240
+ function toggleTheme() {
1241
+ isDark = !isDark;
1242
+ document.body.classList.toggle('light', !isDark);
1243
+ document.getElementById('theme-btn').textContent = isDark ? 'β˜€ Light Mode' : 'πŸŒ™ Dark Mode';
1244
+ }
1245
+
1246
+ // ═══════════════════════════════════════════════════════════
1247
+ // INIT
1248
+ // ═══════════════════════════════════════════════════════════
1249
+ function init() {
1250
+ buildGallery();
1251
+ buildCompareSelect();
1252
+ buildStrengthRadios();
1253
+ buildWalkSelects();
1254
+
1255
+ // Auto-ping server
1256
+ pingServer();
1257
+ }
1258
+
1259
+ document.addEventListener('DOMContentLoaded', init);
1260
+ </script>
1261
+ </body>
1262
+ </html>