| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>ShukGEN β Face Style Generator</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <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" /> |
| <style> |
| |
| |
| |
| :root { |
| --bg-dark: #0D0F18; |
| --bg-panel: #161927; |
| --bg-card: #1E2235; |
| --bg-input: #252A3E; |
| --accent: #7B5CF0; |
| --accent-hover: #9B7FFF; |
| --accent2: #38BDF8; |
| --success: #34D399; |
| --warning: #FBBF24; |
| --danger: #F87171; |
| --text-primary: #EEF0FF; |
| --text-secondary: #8B91B8; |
| --text-muted: #4A5070; |
| --border: #272C42; |
| --sep: #1E2235; |
| |
| --style0: #34D399; |
| --style1: #C0392B; |
| --style2: #8E44AD; |
| --style3: #F59E0B; |
| --style4: #EF4444; |
| --style5: #F97316; |
| --style6: #38BDF8; |
| --style7: #14B8A6; |
| |
| --font-ui: 'DM Sans', sans-serif; |
| --font-mono: 'Space Mono', monospace; |
| |
| --server: ; |
| } |
| |
| |
| body.light { |
| --bg-dark: #F0F2FF; |
| --bg-panel: #E4E6F5; |
| --bg-card: #D8DAF0; |
| --bg-input: #C8CAE0; |
| --accent: #7B5CF0; |
| --accent-hover: #9B7FFF; |
| --accent2: #0284C7; |
| --success: #059669; |
| --warning: #D97706; |
| --danger: #DC2626; |
| --text-primary: #0D0F18; |
| --text-secondary: #3A3D5C; |
| --text-muted: #7A7DA0; |
| --border: #B0B3D0; |
| --sep: #C8CAE0; |
| } |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| html, body { |
| height: 100%; |
| background: var(--bg-dark); |
| color: var(--text-primary); |
| font-family: var(--font-ui); |
| font-size: 14px; |
| overflow: hidden; |
| } |
| |
| |
| #app { display: flex; flex-direction: column; height: 100vh; } |
| |
| |
| #titlebar { |
| display: flex; align-items: center; |
| height: 58px; padding: 0 16px 0 0; |
| background: var(--bg-panel); |
| border-bottom: 1px solid var(--sep); |
| flex-shrink: 0; |
| } |
| .titlebar-strip { |
| width: 5px; height: 58px; flex-shrink: 0; |
| background: linear-gradient(to bottom, var(--accent), var(--accent2)); |
| } |
| .titlebar-logo { |
| display: flex; align-items: baseline; gap: 2px; |
| margin-left: 16px; |
| } |
| .titlebar-logo .name { font-size: 20px; font-weight: 800; letter-spacing: -1px; } |
| .titlebar-logo .ver { font-size: 14px; font-weight: 700; color: var(--accent); padding-top: 2px; } |
| .titlebar-sub { color: var(--text-secondary); font-size: 13px; margin-left: 8px; } |
| .spacer { flex: 1; } |
| .badge { |
| padding: 3px 10px; border-radius: 10px; |
| font-size: 11px; font-weight: 700; color: #fff; |
| background: var(--accent2); |
| } |
| .badge.gpu { background: var(--success); } |
| #theme-btn { |
| margin-left: 12px; |
| background: var(--bg-input); color: var(--text-secondary); |
| border: none; border-radius: 8px; padding: 6px 14px; |
| font-size: 12px; font-family: var(--font-ui); cursor: pointer; |
| transition: background .2s, color .2s; |
| } |
| #theme-btn:hover { background: var(--border); color: var(--text-primary); } |
| |
| |
| #body { display: flex; flex: 1; min-height: 0; } |
| |
| |
| #sidebar { |
| width: 300px; flex-shrink: 0; |
| background: var(--bg-panel); |
| border-right: 1px solid var(--sep); |
| overflow-y: auto; |
| padding: 20px 16px; |
| display: flex; flex-direction: column; gap: 4px; |
| } |
| |
| |
| .section-header { |
| display: flex; align-items: center; gap: 10px; |
| margin-bottom: 6px; |
| } |
| .section-header .bar { |
| width: 3px; height: 18px; border-radius: 2px; |
| background: var(--accent); flex-shrink: 0; |
| } |
| .section-header span { font-size: 13px; font-weight: 700; } |
| |
| .hs { height: 1px; background: var(--sep); margin: 10px 0; } |
| |
| |
| .btn { |
| display: flex; align-items: center; justify-content: center; |
| gap: 6px; width: 100%; padding: 9px 18px; |
| font-size: 13px; font-weight: 600; font-family: var(--font-ui); |
| border: none; border-radius: 8px; cursor: pointer; |
| transition: background .18s, transform .1s, box-shadow .2s; |
| letter-spacing: .3px; text-decoration: none; |
| } |
| .btn:active { transform: scale(.98); } |
| .btn-primary { background: var(--accent); color: #fff; } |
| .btn-primary:hover { background: var(--accent-hover); box-shadow: 0 0 18px rgba(123,92,240,.35); } |
| .btn-success { background: var(--success); color: #fff; } |
| .btn-success:hover { background: #52E8A8; } |
| .btn-secondary { background: var(--bg-input); color: var(--text-primary); } |
| .btn-secondary:hover { background: var(--border); } |
| .btn-ghost { background: transparent; color: var(--text-secondary); |
| border: 1px solid rgba(123,92,240,.27); } |
| .btn-ghost:hover { background: var(--bg-card); } |
| .btn-danger { background: var(--danger); color: #fff; } |
| |
| .label-muted { color: var(--text-muted); font-size: 11px; margin-top: 3px; word-break: break-all; } |
| .label-status { font-size: 11px; font-weight: 600; } |
| .label-status.ok { color: var(--success); } |
| .label-status.err { color: var(--danger); } |
| |
| |
| #input-preview-box { |
| width: 100%; height: 190px; |
| background: var(--bg-input); border-radius: 8px; |
| display: flex; align-items: center; justify-content: center; |
| overflow: hidden; margin-top: 8px; position: relative; |
| } |
| #input-preview-box img { |
| width: 100%; height: 100%; object-fit: contain; |
| } |
| .img-placeholder { |
| color: var(--text-muted); font-size: 12px; text-align: center; |
| pointer-events: none; |
| } |
| |
| |
| .progress-wrap { height: 6px; background: var(--bg-input); border-radius: 4px; overflow: hidden; margin: 6px 0; } |
| .progress-fill { |
| height: 100%; width: 0%; |
| background: linear-gradient(to right, var(--accent), var(--accent2)); |
| border-radius: 4px; transition: width .4s ease; |
| } |
| |
| |
| .quick-guide { |
| background: var(--bg-input); border-radius: 10px; |
| padding: 12px 14px; margin-top: auto; |
| } |
| .quick-guide .title { color: var(--warning); font-size: 13px; font-weight: 700; margin-bottom: 6px; } |
| .quick-guide p { color: var(--text-secondary); font-size: 11px; margin: 2px 0; } |
| |
| |
| #content { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; } |
| |
| |
| .tab-bar { |
| display: flex; background: var(--bg-panel); |
| border-bottom: 1px solid var(--border); |
| flex-shrink: 0; |
| } |
| .tab { |
| padding: 10px 20px; font-size: 12px; font-weight: 500; |
| color: var(--text-secondary); border: none; background: none; |
| cursor: pointer; font-family: var(--font-ui); |
| border-bottom: 2px solid transparent; |
| transition: color .15s, border-color .15s, background .15s; |
| } |
| .tab:hover { background: var(--bg-card); color: var(--text-primary); } |
| .tab.active { color: var(--text-primary); border-bottom-color: var(--accent); background: var(--bg-dark); } |
| |
| .tab-content { display: none; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; } |
| .tab-content.active { display: flex; } |
| |
| |
| .tab-header { |
| display: flex; align-items: center; gap: 12px; |
| padding: 12px 20px; background: var(--bg-panel); |
| border-bottom: 1px solid var(--sep); flex-shrink: 0; |
| } |
| .tab-header .th-title { font-size: 16px; font-weight: 700; } |
| .tab-header .th-sub { font-size: 12px; color: var(--text-muted); } |
| |
| |
| #gallery-scroll { flex: 1; overflow-y: auto; padding: 16px; } |
| #gallery-grid { |
| display: grid; grid-template-columns: repeat(4, 1fr); |
| gap: 12px; |
| } |
| |
| |
| .style-card { |
| background: var(--bg-card); border-radius: 12px; |
| overflow: hidden; display: flex; flex-direction: column; |
| transition: transform .2s, box-shadow .2s; |
| } |
| .style-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,.35); } |
| .card-accent-bar { height: 4px; flex-shrink: 0; } |
| .card-img-wrap { |
| width: 100%; aspect-ratio: 1; |
| background: var(--bg-card); |
| display: flex; align-items: center; justify-content: center; |
| overflow: hidden; cursor: pointer; |
| } |
| .card-img-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; } |
| .card-img-placeholder { |
| color: var(--text-muted); font-size: 11px; text-align: center; padding: 8px; |
| } |
| .card-meta { padding: 6px 10px 0; } |
| .card-style-num { font-size: 11px; font-weight: 700; } |
| .card-style-name { font-size: 12px; font-weight: 600; margin-top: 2px; } |
| .card-btns { |
| display: flex; gap: 6px; padding: 6px 8px 10px; |
| } |
| .card-btn { |
| flex: 1; padding: 4px 8px; font-size: 11px; font-weight: 600; |
| border: none; border-radius: 6px; cursor: pointer; font-family: var(--font-ui); |
| background: var(--bg-input); color: var(--text-secondary); |
| transition: background .15s, color .15s; |
| } |
| .card-btn:hover { background: var(--border); color: var(--text-primary); } |
| .card-btn.save { color: #fff; } |
| |
| |
| #compare-area { |
| flex: 1; overflow-y: auto; padding: 20px; |
| display: flex; gap: 16px; align-items: flex-start; |
| } |
| .compare-panel { |
| background: var(--bg-card); border-radius: 12px; overflow: hidden; |
| flex: 1; max-width: 320px; display: flex; flex-direction: column; |
| } |
| .compare-panel-header { |
| padding: 10px 14px; font-size: 12px; font-weight: 700; |
| border-bottom: 3px solid var(--accent); |
| } |
| .compare-img-wrap { |
| width: 100%; aspect-ratio: 1; background: var(--bg-card); |
| display: flex; align-items: center; justify-content: center; overflow: hidden; |
| } |
| .compare-img-wrap img { width: 100%; height: 100%; object-fit: cover; } |
| .compare-panel-footer { padding: 8px; } |
| .compare-panel-footer button { |
| width: 100%; padding: 6px 12px; font-size: 11px; |
| background: var(--bg-input); color: var(--text-secondary); |
| border: none; border-radius: 6px; cursor: pointer; font-family: var(--font-ui); |
| } |
| .compare-panel-footer button:hover { background: var(--border); color: var(--text-primary); } |
| .compare-select { |
| padding: 8px; |
| display: flex; align-items: center; gap: 8px; |
| } |
| .compare-select label { font-size: 12px; color: var(--text-secondary); flex-shrink: 0; } |
| .compare-select select { |
| flex: 1; padding: 5px 8px; |
| background: var(--bg-input); color: var(--text-primary); |
| border: 1px solid var(--border); border-radius: 6px; |
| font-size: 12px; font-family: var(--font-ui); |
| } |
| |
| |
| #strength-area { |
| flex: 1; overflow-y: auto; padding: 24px; |
| display: flex; gap: 40px; align-items: flex-start; flex-wrap: wrap; |
| } |
| .strength-controls { display: flex; flex-direction: column; gap: 10px; min-width: 220px; } |
| .strength-controls label { font-size: 13px; font-weight: 700; } |
| .radio-group { display: flex; flex-direction: column; gap: 6px; } |
| .radio-item { |
| display: flex; align-items: center; gap: 8px; |
| font-size: 12px; color: var(--text-secondary); cursor: pointer; |
| } |
| .radio-item input[type=radio] { accent-color: var(--accent); width: 16px; height: 16px; } |
| .radio-item:has(input:checked) { color: var(--text-primary); } |
| .alpha-display { |
| font-size: 28px; font-weight: 800; color: var(--accent); |
| font-family: var(--font-mono); letter-spacing: -1px; |
| } |
| input[type=range] { |
| width: 280px; accent-color: var(--accent); |
| cursor: pointer; |
| } |
| .preset-row { display: flex; gap: 6px; flex-wrap: wrap; } |
| .preset-btn { |
| padding: 4px 10px; border-radius: 6px; font-size: 11px; |
| background: var(--bg-input); color: var(--text-secondary); |
| border: none; cursor: pointer; font-family: var(--font-ui); |
| transition: background .15s; |
| } |
| .preset-btn:hover { background: var(--border); color: var(--text-primary); } |
| .strength-previews { display: flex; gap: 12px; flex-wrap: wrap; } |
| .strength-preview-card { |
| background: var(--bg-card); border-radius: 10px; overflow: hidden; |
| display: flex; flex-direction: column; width: 200px; |
| } |
| .strength-preview-card .card-label { |
| padding: 6px 10px; font-size: 11px; font-weight: 700; |
| color: var(--text-secondary); background: var(--bg-panel); |
| } |
| .strength-preview-card img, |
| .strength-preview-card .img-ph { |
| width: 100%; aspect-ratio: 1; object-fit: cover; |
| background: var(--bg-input); display: flex; |
| align-items: center; justify-content: center; |
| color: var(--text-muted); font-size: 11px; text-align: center; |
| } |
| |
| |
| #walk-controls { |
| display: flex; align-items: center; gap: 20px; flex-wrap: wrap; |
| padding: 16px 20px; background: var(--bg-panel); |
| border-bottom: 1px solid var(--sep); flex-shrink: 0; |
| } |
| #walk-controls label { font-size: 12px; color: var(--text-secondary); } |
| #walk-controls select, #walk-controls input[type=number] { |
| padding: 5px 10px; |
| background: var(--bg-input); color: var(--text-primary); |
| border: 1px solid var(--border); border-radius: 6px; |
| font-size: 12px; font-family: var(--font-ui); |
| } |
| #walk-scroll { flex: 1; overflow: auto; padding: 16px; } |
| #walk-frames-row { display: flex; gap: 8px; flex-wrap: wrap; } |
| .walk-frame-card { |
| background: var(--bg-card); border-radius: 8px; overflow: hidden; |
| display: flex; flex-direction: column; width: 150px; flex-shrink: 0; |
| } |
| .walk-frame-card .wf-bar { height: 3px; } |
| .walk-frame-card img { width: 150px; height: 150px; object-fit: cover; } |
| .walk-frame-card .wf-label { |
| text-align: center; font-size: 10px; color: var(--text-muted); |
| padding: 4px 0; font-family: var(--font-mono); |
| } |
| |
| |
| #statusbar { |
| height: 32px; padding: 0 16px; |
| background: var(--bg-panel); border-top: 1px solid var(--sep); |
| display: flex; align-items: center; gap: 12px; flex-shrink: 0; |
| font-size: 11px; color: var(--text-muted); |
| } |
| #status-text { flex: 1; } |
| #server-indicator { font-size: 11px; font-weight: 600; } |
| #server-indicator.ok { color: var(--success); } |
| #server-indicator.err { color: var(--danger); } |
| |
| |
| #lightbox { |
| display: none; position: fixed; inset: 0; |
| background: rgba(0,0,0,.85); z-index: 9999; |
| align-items: center; justify-content: center; |
| cursor: zoom-out; |
| } |
| #lightbox.show { display: flex; } |
| #lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 8px; object-fit: contain; } |
| |
| |
| ::-webkit-scrollbar { width: 8px; height: 8px; } |
| ::-webkit-scrollbar-track { background: var(--bg-panel); } |
| ::-webkit-scrollbar-thumb { background: var(--bg-input); border-radius: 4px; } |
| ::-webkit-scrollbar-thumb:hover { background: var(--accent); } |
| |
| |
| .server-row { |
| display: flex; align-items: center; gap: 6px; margin-bottom: 6px; |
| } |
| .server-row input { |
| flex: 1; padding: 6px 10px; |
| background: var(--bg-input); color: var(--text-primary); |
| border: 1px solid var(--border); border-radius: 6px; |
| font-size: 11px; font-family: var(--font-mono); |
| } |
| .server-row button { |
| padding: 6px 10px; background: var(--accent); color: #fff; |
| border: none; border-radius: 6px; font-size: 11px; cursor: pointer; |
| } |
| |
| |
| .path-row { |
| display: flex; gap: 6px; align-items: stretch; margin-bottom: 4px; |
| } |
| .path-row input { |
| flex: 1; padding: 7px 10px; |
| background: var(--bg-input); color: var(--text-primary); |
| border: 1px solid var(--border); border-radius: 8px; |
| font-size: 11px; font-family: var(--font-mono); |
| min-width: 0; |
| } |
| .path-row input::placeholder { color: var(--text-muted); } |
| .path-row button { |
| padding: 7px 12px; background: var(--accent); color: #fff; |
| border: none; border-radius: 8px; font-size: 12px; font-weight: 600; |
| cursor: pointer; white-space: nowrap; |
| } |
| .path-row button:hover { background: var(--accent-hover); } |
| |
| |
| #drop-zone { |
| border: 2px dashed var(--border); border-radius: 8px; |
| padding: 16px 10px; text-align: center; cursor: pointer; |
| color: var(--text-muted); font-size: 12px; |
| transition: border-color .2s, background .2s; |
| margin-top: 8px; |
| } |
| #drop-zone.over { border-color: var(--accent); background: rgba(123,92,240,.08); } |
| #drop-zone input { display: none; } |
| |
| @media (max-width: 900px) { |
| #gallery-grid { grid-template-columns: repeat(2, 1fr); } |
| #sidebar { width: 260px; } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div id="lightbox" onclick="closeLightbox()"> |
| <img id="lightbox-img" src="" alt="Preview" /> |
| </div> |
|
|
| <div id="app"> |
|
|
| |
| <div id="titlebar"> |
| <div class="titlebar-strip"></div> |
| <div class="titlebar-logo"> |
| <span class="name">ShukGEN</span> |
| </div> |
| <span class="titlebar-sub">β Face Style Generator</span> |
| <div class="spacer"></div> |
| <span class="badge" id="device-badge">π₯ CPU</span> |
| <button id="theme-btn" onclick="toggleTheme()">β Light Mode</button> |
| </div> |
|
|
| |
| <div id="body"> |
|
|
| |
| <div id="sidebar"> |
|
|
| |
| <div class="section-header"> |
| <div class="bar"></div><span>π Server</span> |
| </div> |
| <div class="server-row"> |
| <input id="server-url-input" type="text" value="" /> |
| <button onclick="pingServer()">Connect</button> |
| </div> |
| <div id="server-status" class="label-muted">Not connected β start server.py first</div> |
|
|
| <div class="hs"></div> |
|
|
| |
| <div class="section-header"> |
| <div class="bar"></div><span>β Load Model</span> |
| </div> |
| <div class="path-row"> |
| <input id="model-path-input" type="text" placeholder="Paste full path to .pth fileβ¦" /> |
| <button onclick="loadModel()">Load</button> |
| </div> |
| <div id="model-path-label" class="label-muted">No model loaded</div> |
| <div id="model-badge" class="label-status err">β Not Loaded</div> |
|
|
| <div class="hs"></div> |
|
|
| |
| <div class="section-header"> |
| <div class="bar"></div><span>β‘ Upload Face Image</span> |
| </div> |
| <div id="drop-zone" onclick="document.getElementById('file-input').click()" |
| ondragover="event.preventDefault(); this.classList.add('over')" |
| ondragleave="this.classList.remove('over')" |
| ondrop="handleDrop(event)"> |
| <input type="file" id="file-input" accept="image/*" onchange="handleFileSelect(event)" /> |
| πΌ Click or drag & drop a face image |
| </div> |
| <div id="img-path-label" class="label-muted" style="margin-top:4px;">No image selected</div> |
| <div id="input-preview-box"> |
| <span class="img-placeholder">Upload a face image</span> |
| </div> |
|
|
| <div class="hs"></div> |
|
|
| |
| <div class="section-header"> |
| <div class="bar"></div><span>β’ Generate Styles</span> |
| </div> |
| <button class="btn btn-success" onclick="generateAll()">β¨ Generate All 8 Styles</button> |
| <div class="progress-wrap" style="margin-top:6px;"> |
| <div class="progress-fill" id="progress-fill"></div> |
| </div> |
| <div id="gen-status" class="label-muted">Ready</div> |
|
|
| <div class="hs"></div> |
|
|
| |
| <div class="section-header"> |
| <div class="bar"></div><span>β£ Export & Tools</span> |
| </div> |
| <button class="btn btn-secondary" onclick="saveAll()" style="margin-bottom:4px;">πΎ Save All Outputs</button> |
| <button class="btn btn-ghost" onclick="showAbout()">βΉοΈ Architecture Info</button> |
|
|
| |
| <div class="quick-guide" style="margin-top:16px;"> |
| <div class="title">π‘ Quick Guide</div> |
| <p>1. Start server.py, then Connect</p> |
| <p>2. Paste .pth model path, click Load</p> |
| <p>3. Upload a face photo</p> |
| <p>4. Click Generate All Styles</p> |
| <p>5. Preview / Save any result</p> |
| <p>6. Try Strength & Walk tabs!</p> |
| </div> |
|
|
| </div> |
|
|
| |
| <div id="content"> |
|
|
| |
| <div class="tab-bar"> |
| <button class="tab active" onclick="switchTab('gallery', this)">π¨ Style Gallery</button> |
| <button class="tab" onclick="switchTab('compare', this)">π Compare</button> |
| <button class="tab" onclick="switchTab('strength', this)">π Strength</button> |
| <button class="tab" onclick="switchTab('walk', this)">πΆ Latent Walk</button> |
| </div> |
|
|
| |
| <div class="tab-content active" id="tab-gallery"> |
| <div class="tab-header"> |
| <span class="th-title">8 Visually Distinct Style Variants</span> |
| <span class="th-sub">Each style applies different pixel transforms β mathematically guaranteed to look distinct</span> |
| </div> |
| <div id="gallery-scroll"> |
| <div id="gallery-grid"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-compare"> |
| <div class="tab-header"> |
| <span class="th-title">Original vs Reconstruction vs Styled</span> |
| </div> |
| <div id="compare-area"> |
| |
| <div class="compare-panel"> |
| <div class="compare-panel-header" style="border-color: var(--accent2);">Original Input</div> |
| <div class="compare-img-wrap" id="cmp-orig-wrap"> |
| <span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Upload image first</span> |
| </div> |
| <div class="compare-panel-footer"> |
| <button onclick="openLightbox('orig')">π Full Size Preview</button> |
| </div> |
| </div> |
| |
| <div class="compare-panel"> |
| <div class="compare-panel-header" style="border-color: var(--success);">VAE Reconstruction</div> |
| <div class="compare-img-wrap" id="cmp-recon-wrap"> |
| <span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Generate first</span> |
| </div> |
| <div class="compare-panel-footer"> |
| <button onclick="openLightbox('recon')">π Full Size Preview</button> |
| </div> |
| </div> |
| |
| <div class="compare-panel"> |
| <div class="compare-panel-header" style="border-color: var(--accent);">Styled Version</div> |
| <div class="compare-img-wrap" id="cmp-styled-wrap"> |
| <span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Generate first</span> |
| </div> |
| <div class="compare-select"> |
| <label>Style:</label> |
| <select id="compare-style-sel" onchange="updateCompareStyled()"></select> |
| </div> |
| <div class="compare-panel-footer"> |
| <button onclick="openLightbox('styled')">π Full Size Preview</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-strength"> |
| <div class="tab-header"> |
| <span class="th-title">Style Strength Control (Ξ±-Blending)</span> |
| <span class="th-sub">Ξ±=0 β pure reconstruction | Ξ±=1 β full style effect</span> |
| </div> |
| <div id="strength-area"> |
| <div class="strength-controls"> |
| <label>Select Style:</label> |
| <div class="radio-group" id="strength-radio-group"> |
| |
| </div> |
| <div style="margin-top:16px;"> |
| <label>Blend Strength (Ξ±):</label> |
| <div class="alpha-display" id="alpha-display">Ξ± = 1.00</div> |
| <input type="range" id="alpha-slider" min="0" max="100" value="100" |
| oninput="onAlphaChange(this.value)" style="margin: 8px 0;" /> |
| <div class="preset-row"> |
| <button class="preset-btn" onclick="setAlpha(0)">0%</button> |
| <button class="preset-btn" onclick="setAlpha(25)">25%</button> |
| <button class="preset-btn" onclick="setAlpha(50)">50%</button> |
| <button class="preset-btn" onclick="setAlpha(75)">75%</button> |
| <button class="preset-btn" onclick="setAlpha(100)">100%</button> |
| </div> |
| </div> |
| <button class="btn btn-primary" onclick="updateStrengthPreview()" style="margin-top:12px;"> |
| π Update Preview |
| </button> |
| </div> |
| <div class="strength-previews"> |
| <div class="strength-preview-card"> |
| <div class="card-label">Reconstruction</div> |
| <div id="sp-recon" class="img-ph" style="display:flex;align-items:center;justify-content:center;min-height:200px;">Generate first</div> |
| </div> |
| <div class="strength-preview-card"> |
| <div class="card-label">Blended (Ξ±)</div> |
| <div id="sp-blend" class="img-ph" style="display:flex;align-items:center;justify-content:center;min-height:200px;">Generate first</div> |
| </div> |
| <div class="strength-preview-card"> |
| <div class="card-label">Full Style</div> |
| <div id="sp-styled" class="img-ph" style="display:flex;align-items:center;justify-content:center;min-height:200px;">Generate first</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-walk"> |
| <div class="tab-header"> |
| <span class="th-title">Latent Space Walk</span> |
| <span class="th-sub">Traverse the VAE latent space between two style directions</span> |
| </div> |
| <div id="walk-controls"> |
| <label>Style A: |
| <select id="walk-style-a"></select> |
| </label> |
| <label>Style B: |
| <select id="walk-style-b"></select> |
| </label> |
| <label>Steps: |
| <input type="number" id="walk-steps" value="7" min="2" max="16" style="width:60px;" /> |
| </label> |
| <button class="btn btn-primary" style="width:auto;padding:8px 20px;" onclick="runWalk()"> |
| πΆ Run Walk |
| </button> |
| <span id="walk-status" style="font-size:11px;color:var(--text-muted);">Generate styles first</span> |
| </div> |
| <div id="walk-scroll"> |
| <div id="walk-frames-row"> |
| <span style="color:var(--text-muted);font-size:12px;">Run a latent walk to see frames here.</span> |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| |
| <div id="statusbar"> |
| <span id="status-text">Ready β start server.py and click Connect</span> |
| <span id="server-indicator" class="err">β Disconnected</span> |
| </div> |
|
|
| </div> |
|
|
| |
| <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;"> |
| <div style="background:var(--bg-dark);border-radius:12px;max-width:560px;width:90%;max-height:80vh;overflow-y:auto;padding:24px;"> |
| <h2 style="font-size:18px;font-weight:800;margin-bottom:8px;">ShukGEN β Architecture Info</h2> |
| <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> |
| <button class="btn btn-secondary" style="margin-top:16px;" onclick="document.getElementById('about-overlay').style.display='none'">Close</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| |
| |
| const STYLE_NAMES = [ |
| "Youthful", "Aged / Mature", "Dramatic Light", "Soft Glow", |
| "Intense / Bold", "Warm Golden Hour", "Cool / Moody", "Sketch / Artistic", |
| ]; |
| const STYLE_ICONS = ["πΈ","πΏ","β‘","β¨","π₯","π
","π","π¨"]; |
| const STYLE_COLORS = ["#34D399","#C0392B","#8E44AD","#F59E0B","#EF4444","#F97316","#38BDF8","#14B8A6"]; |
| |
| |
| |
| |
| const S = { |
| serverURL: '', |
| modelLoaded: false, |
| imageLoaded: false, |
| generated: false, |
| origB64: null, |
| reconB64: null, |
| styledB64: new Array(8).fill(null), |
| uploadedImg: null, |
| }; |
| |
| |
| |
| |
| function getURL() { |
| return document.getElementById('server-url-input').value.replace(/\/$/, ''); |
| } |
| |
| async function apiFetch(path, opts = {}) { |
| const url = getURL() + path; |
| const res = await fetch(url, { |
| headers: { 'Content-Type': 'application/json' }, |
| ...opts, |
| }); |
| return res.json(); |
| } |
| |
| |
| |
| |
| async function pingServer() { |
| setStatus('Connecting to serverβ¦'); |
| try { |
| const data = await apiFetch('/status'); |
| document.getElementById('server-status').textContent = 'β
Connected'; |
| document.getElementById('server-indicator').textContent = 'β Connected'; |
| document.getElementById('server-indicator').className = 'ok'; |
| |
| |
| const badge = document.getElementById('device-badge'); |
| if (data.device && data.device.startsWith('GPU')) { |
| badge.textContent = 'β‘ ' + data.device; |
| badge.classList.add('gpu'); |
| } else { |
| badge.textContent = 'π₯ ' + (data.device || 'CPU'); |
| } |
| |
| setStatus(`Connected β PyTorch ${data.torch_version} | ${data.device}`); |
| |
| if (data.model_loaded) { |
| S.modelLoaded = true; |
| document.getElementById('model-badge').textContent = 'β Loaded'; |
| document.getElementById('model-badge').className = 'label-status ok'; |
| const cfg = data.model_config; |
| document.getElementById('model-path-label').textContent = |
| `latent_dim=${cfg.latent_dim} | base_filters=${cfg.base_filters} | img_size=${data.image_size || '?'}`; |
| } |
| } catch (e) { |
| document.getElementById('server-status').textContent = 'β Cannot reach server β is server.py running?'; |
| document.getElementById('server-indicator').textContent = 'β Disconnected'; |
| document.getElementById('server-indicator').className = 'err'; |
| setStatus('Cannot reach server. Start server.py first.'); |
| } |
| } |
| |
| |
| |
| |
| async function loadModel() { |
| const path = document.getElementById('model-path-input').value.trim(); |
| if (!path) { alert('Please paste the full path to your .pth file.'); return; } |
| setStatus('β³ Loading modelβ¦'); |
| try { |
| const data = await apiFetch('/load_model', { |
| method: 'POST', |
| body: JSON.stringify({ path }), |
| }); |
| if (data.error) { |
| setStatus('β ' + data.error); |
| document.getElementById('model-badge').textContent = 'β Load Failed'; |
| document.getElementById('model-badge').className = 'label-status err'; |
| alert('Model load error:\n' + data.error); |
| return; |
| } |
| S.modelLoaded = true; |
| document.getElementById('model-badge').textContent = 'β Loaded β'; |
| document.getElementById('model-badge').className = 'label-status ok'; |
| document.getElementById('model-path-label').textContent = |
| `${data.filename} | latent=${data.config.latent_dim} base_f=${data.config.base_filters} img=${data.image_size}`; |
| setStatus(`β
Model loaded: ${data.filename}`); |
| } catch (e) { |
| setStatus('β Network error β is server running?'); |
| } |
| } |
| |
| |
| |
| |
| function handleDrop(e) { |
| e.preventDefault(); |
| document.getElementById('drop-zone').classList.remove('over'); |
| const file = e.dataTransfer.files[0]; |
| if (file && file.type.startsWith('image/')) loadImageFile(file); |
| } |
| function handleFileSelect(e) { |
| const file = e.target.files[0]; |
| if (file) loadImageFile(file); |
| } |
| |
| function loadImageFile(file) { |
| const reader = new FileReader(); |
| reader.onload = async (ev) => { |
| const b64 = ev.target.result; |
| S.origB64 = b64; |
| S.imageLoaded = true; |
| document.getElementById('img-path-label').textContent = file.name; |
| |
| |
| const box = document.getElementById('input-preview-box'); |
| box.innerHTML = `<img src="${b64}" style="width:100%;height:100%;object-fit:contain;" />`; |
| |
| |
| updateCompareOrig(b64); |
| |
| setStatus('πΌ Image loaded: ' + file.name); |
| |
| |
| try { |
| const data = await apiFetch('/load_image', { |
| method: 'POST', |
| body: JSON.stringify({ image_b64: b64 }), |
| }); |
| if (data.error) setStatus('β Image upload error: ' + data.error); |
| } catch (e) { |
| setStatus('β Could not send image to server β check connection.'); |
| } |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| |
| |
| |
| async function generateAll() { |
| if (!S.modelLoaded) { alert('Load a model first.'); return; } |
| if (!S.imageLoaded) { alert('Upload an image first.'); return; } |
| |
| setProgress(0); |
| setGenStatus('β³ Encoding face with VAEβ¦'); |
| setStatus('β³ Encoding face with VAEβ¦'); |
| |
| |
| for (let i = 0; i < 8; i++) setCardLoading(i); |
| |
| try { |
| const data = await apiFetch('/generate', { method: 'POST', body: '{}' }); |
| if (data.error) { |
| setGenStatus('β Error'); |
| setStatus('β Generate error: ' + data.error); |
| alert('Generation error:\n' + data.error); |
| return; |
| } |
| |
| S.reconB64 = data.recon; |
| S.styledB64 = data.styles; |
| S.generated = true; |
| |
| setProgress(100); |
| |
| |
| for (let i = 0; i < 8; i++) setCardImage(i, data.styles[i]); |
| |
| |
| updateCompareRecon(data.recon); |
| updateCompareStyled(); |
| |
| |
| setImgOrPh('sp-recon', data.recon); |
| |
| setGenStatus('β
Done β 8 styles generated!'); |
| setStatus('β
All 8 styles generated successfully.'); |
| } catch (e) { |
| setGenStatus('β Network error'); |
| setStatus('β Generation failed β is server running?'); |
| } |
| } |
| |
| |
| |
| |
| function onAlphaChange(val) { |
| const v = (val / 100).toFixed(2); |
| document.getElementById('alpha-display').textContent = `Ξ± = ${v}`; |
| } |
| function setAlpha(val) { |
| document.getElementById('alpha-slider').value = val; |
| onAlphaChange(val); |
| } |
| |
| async function updateStrengthPreview() { |
| if (!S.generated) { alert('Generate styles first.'); return; } |
| const styleIdx = getCheckedRadio(); |
| const alpha = document.getElementById('alpha-slider').value / 100; |
| |
| |
| setImgOrPh('sp-recon', S.reconB64); |
| |
| setImgOrPh('sp-styled', S.styledB64[styleIdx]); |
| |
| |
| try { |
| const data = await apiFetch('/strength_preview', { |
| method: 'POST', |
| body: JSON.stringify({ style_idx: styleIdx, alpha }), |
| }); |
| if (data.blend) setImgOrPh('sp-blend', data.blend); |
| } catch (e) { |
| setStatus('β Strength preview failed'); |
| } |
| } |
| |
| function getCheckedRadio() { |
| const radios = document.querySelectorAll('#strength-radio-group input[type=radio]'); |
| for (let r of radios) if (r.checked) return parseInt(r.value); |
| return 0; |
| } |
| |
| |
| |
| |
| async function runWalk() { |
| if (!S.generated) { alert('Generate styles first.'); return; } |
| const styleA = parseInt(document.getElementById('walk-style-a').value); |
| const styleB = parseInt(document.getElementById('walk-style-b').value); |
| const steps = parseInt(document.getElementById('walk-steps').value); |
| document.getElementById('walk-status').textContent = 'β³ Runningβ¦'; |
| document.getElementById('walk-frames-row').innerHTML = |
| '<span style="color:var(--text-muted);font-size:12px;">Generating framesβ¦</span>'; |
| |
| try { |
| const data = await apiFetch('/latent_walk', { |
| method: 'POST', |
| body: JSON.stringify({ style_a: styleA, style_b: styleB, steps }), |
| }); |
| if (data.error) { |
| document.getElementById('walk-status').textContent = 'β ' + data.error; |
| return; |
| } |
| const row = document.getElementById('walk-frames-row'); |
| row.innerHTML = ''; |
| data.frames.forEach((f, i) => { |
| const color = f.alpha < 0.5 ? STYLE_COLORS[styleA] : STYLE_COLORS[styleB]; |
| const card = document.createElement('div'); |
| card.className = 'walk-frame-card'; |
| card.innerHTML = ` |
| <div class="wf-bar" style="background:${color};"></div> |
| <img src="${f.img}" loading="lazy" onclick="openLightboxSrc('${f.img}')" style="cursor:zoom-in;" /> |
| <div class="wf-label">Ξ±=${f.alpha.toFixed(2)}</div> |
| `; |
| row.appendChild(card); |
| }); |
| document.getElementById('walk-status').textContent = |
| `β
${data.frames.length} frames: ${STYLE_NAMES[styleA]} β ${STYLE_NAMES[styleB]}`; |
| setStatus(`β
Latent walk: ${data.frames.length} frames`); |
| } catch (e) { |
| document.getElementById('walk-status').textContent = 'β Network error'; |
| setStatus('β Walk failed β is server running?'); |
| } |
| } |
| |
| |
| |
| |
| function updateCompareOrig(b64) { |
| setImgInWrap('cmp-orig-wrap', b64, 'Original'); |
| } |
| function updateCompareRecon(b64) { |
| setImgInWrap('cmp-recon-wrap', b64, 'Reconstruction'); |
| } |
| function updateCompareStyled() { |
| const idx = parseInt(document.getElementById('compare-style-sel').value); |
| const b64 = S.styledB64[idx]; |
| if (b64) setImgInWrap('cmp-styled-wrap', b64, STYLE_NAMES[idx]); |
| else { |
| document.getElementById('cmp-styled-wrap').innerHTML = |
| `<span class="img-placeholder" style="color:var(--text-muted);font-size:11px;">Generate first</span>`; |
| } |
| } |
| |
| function setImgInWrap(id, b64, label) { |
| const wrap = document.getElementById(id); |
| wrap.innerHTML = `<img src="${b64}" style="width:100%;height:100%;object-fit:cover;" alt="${label}" onclick="openLightboxSrc('${b64}')" style="cursor:zoom-in;" />`; |
| } |
| |
| |
| |
| |
| function downloadB64(b64, filename) { |
| const a = document.createElement('a'); |
| a.href = b64; |
| a.download = filename; |
| a.click(); |
| } |
| |
| function saveStyle(idx) { |
| const b64 = S.styledB64[idx]; |
| if (!b64) { alert('Generate styles first.'); return; } |
| const name = STYLE_NAMES[idx].replace(/[/ ]+/g, '_').toLowerCase(); |
| downloadB64(b64, `shukgen_v3_style_${idx}_${name}.png`); |
| setStatus(`πΎ Saved style ${idx}: ${STYLE_NAMES[idx]}`); |
| } |
| |
| async function saveAll() { |
| if (!S.generated && !S.imageLoaded) { alert('Generate styles first.'); return; } |
| if (S.origB64) downloadB64(S.origB64, '00_original.png'); |
| if (S.reconB64) downloadB64(S.reconB64, '01_reconstruction.png'); |
| S.styledB64.forEach((b64, i) => { |
| if (b64) { |
| const name = STYLE_NAMES[i].replace(/[/ ]+/g, '_').toLowerCase(); |
| setTimeout(() => downloadB64(b64, `style_${String(i).padStart(2,'0')}_${name}.png`), i * 100); |
| } |
| }); |
| setStatus('πΎ Saving all generated imagesβ¦'); |
| } |
| |
| |
| |
| |
| const _lightboxSrcs = {}; |
| function openLightbox(key) { |
| const srcs = { orig: S.origB64, recon: S.reconB64, styled: S.styledB64[parseInt(document.getElementById('compare-style-sel').value)] }; |
| const src = srcs[key]; |
| if (!src) return; |
| openLightboxSrc(src); |
| } |
| function openLightboxSrc(src) { |
| document.getElementById('lightbox-img').src = src; |
| document.getElementById('lightbox').classList.add('show'); |
| } |
| function closeLightbox() { |
| document.getElementById('lightbox').classList.remove('show'); |
| } |
| |
| |
| |
| |
| function buildGallery() { |
| const grid = document.getElementById('gallery-grid'); |
| grid.innerHTML = ''; |
| for (let i = 0; i < 8; i++) { |
| const color = STYLE_COLORS[i]; |
| const card = document.createElement('div'); |
| card.className = 'style-card'; |
| card.id = `card-${i}`; |
| card.innerHTML = ` |
| <div class="card-accent-bar" style="background:${color};"></div> |
| <div class="card-img-wrap" id="card-img-wrap-${i}" onclick="previewStyle(${i})"> |
| <span class="card-img-placeholder">Pendingβ¦</span> |
| </div> |
| <div class="card-meta"> |
| <div class="card-style-num" style="color:${color};">${STYLE_ICONS[i]} Style ${i}</div> |
| <div class="card-style-name">${STYLE_NAMES[i]}</div> |
| </div> |
| <div class="card-btns"> |
| <button class="card-btn" onclick="previewStyle(${i})">π Preview</button> |
| <button class="card-btn save" style="background:${color};" onclick="saveStyle(${i})">πΎ Save</button> |
| </div> |
| `; |
| grid.appendChild(card); |
| } |
| } |
| |
| function setCardLoading(i) { |
| document.getElementById(`card-img-wrap-${i}`).innerHTML = |
| '<span class="card-img-placeholder">Generatingβ¦</span>'; |
| } |
| function setCardImage(i, b64) { |
| document.getElementById(`card-img-wrap-${i}`).innerHTML = |
| `<img src="${b64}" alt="${STYLE_NAMES[i]}" />`; |
| } |
| function previewStyle(i) { |
| if (!S.styledB64[i]) { alert('Generate styles first.'); return; } |
| openLightboxSrc(S.styledB64[i]); |
| } |
| |
| |
| |
| |
| function buildCompareSelect() { |
| const sel = document.getElementById('compare-style-sel'); |
| sel.innerHTML = ''; |
| STYLE_NAMES.forEach((n, i) => { |
| const opt = document.createElement('option'); |
| opt.value = i; opt.textContent = `${i}: ${n}`; |
| sel.appendChild(opt); |
| }); |
| } |
| |
| |
| |
| |
| function buildStrengthRadios() { |
| const group = document.getElementById('strength-radio-group'); |
| group.innerHTML = ''; |
| STYLE_NAMES.forEach((n, i) => { |
| const lbl = document.createElement('label'); |
| lbl.className = 'radio-item'; |
| lbl.innerHTML = ` |
| <input type="radio" name="strength-style" value="${i}" ${i===0?'checked':''} /> |
| ${STYLE_ICONS[i]} ${n} |
| `; |
| group.appendChild(lbl); |
| }); |
| } |
| |
| |
| |
| |
| function buildWalkSelects() { |
| ['walk-style-a','walk-style-b'].forEach((id, j) => { |
| const sel = document.getElementById(id); |
| sel.innerHTML = ''; |
| STYLE_NAMES.forEach((n, i) => { |
| const opt = document.createElement('option'); |
| opt.value = i; opt.textContent = `${i}: ${n}`; |
| if (j === 0 && i === 0) opt.selected = true; |
| if (j === 1 && i === 1) opt.selected = true; |
| sel.appendChild(opt); |
| }); |
| }); |
| } |
| |
| |
| |
| |
| function showAbout() { |
| const overlay = document.getElementById('about-overlay'); |
| overlay.style.display = 'flex'; |
| fetch(getURL() + '/status').then(r => r.json()).then(data => { |
| const cfg = data.model_config || {}; |
| document.getElementById('about-text').textContent = [ |
| 'ShukGEN β Face Style Generator', |
| 'ββββββββββββββββββββββββββββββββββββ', |
| '', |
| 'MODEL ARCHITECTURE: FaceVAE', |
| ` Latent Dimension : ${cfg.latent_dim ?? 'β'}`, |
| ` Base Filters : ${cfg.base_filters ?? 'β'}`, |
| ` Image Size : ${data.image_size ?? 'β'}`, |
| ` Device : ${data.device ?? 'β'}`, |
| ` PyTorch : ${data.torch_version ?? 'β'}`, |
| '', |
| 'ENCODER', |
| ' Inc β DownBlockΓ4 β Bottleneck β Pool β fc_mu / fc_logvar', |
| ' Bottleneck: ResBlock β AttnBlock β ResBlock β SE', |
| '', |
| 'DECODER', |
| ' fc_dec β Upsample β UpBlockΓ4 β outc (Tanh)', |
| '', |
| 'STYLE TRANSFORMS (8 built-in, pixel-space):', |
| ' 0: Youthful β saturation boost + subtle smoothing', |
| ' 1: Aged / Mature β desaturation + warm tint + grain + vignette', |
| ' 2: Dramatic Light β horizontal gradient + high contrast + vignette', |
| ' 3: Soft Glow β blur additive blend + warm tint + lift', |
| ' 4: Intense / Bold β heavy saturation + contrast + sharpness', |
| ' 5: Warm Golden Hr β red/green boost + blue reduction', |
| ' 6: Cool / Moody β red reduction + blue boost + grain + vignette', |
| ' 7: Sketch/Artisticβ posterize + edge detection + canvas grain', |
| '', |
| 'LATENT WALK', |
| ' Random direction sampled in latent space.', |
| ' Walk range: β2.5Ο to +2.5Ο, styles blended by Ξ±.', |
| ].join('\n'); |
| }).catch(() => { |
| document.getElementById('about-text').textContent = 'Connect to server to see architecture details.'; |
| }); |
| } |
| |
| |
| |
| |
| function setStatus(msg) { |
| document.getElementById('status-text').textContent = msg; |
| } |
| function setGenStatus(msg) { |
| document.getElementById('gen-status').textContent = msg; |
| } |
| function setProgress(pct) { |
| document.getElementById('progress-fill').style.width = pct + '%'; |
| } |
| function setImgOrPh(id, b64) { |
| const el = document.getElementById(id); |
| if (b64) { |
| el.outerHTML = `<img id="${id}" src="${b64}" style="width:100%;aspect-ratio:1;object-fit:cover;cursor:zoom-in;" onclick="openLightboxSrc('${b64}')" />`; |
| } |
| } |
| |
| |
| function switchTab(name, btn) { |
| document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| document.getElementById('tab-' + name).classList.add('active'); |
| btn.classList.add('active'); |
| } |
| |
| |
| let isDark = true; |
| function toggleTheme() { |
| isDark = !isDark; |
| document.body.classList.toggle('light', !isDark); |
| document.getElementById('theme-btn').textContent = isDark ? 'β Light Mode' : 'π Dark Mode'; |
| } |
| |
| |
| |
| |
| function init() { |
| buildGallery(); |
| buildCompareSelect(); |
| buildStrengthRadios(); |
| buildWalkSelects(); |
| |
| |
| pingServer(); |
| } |
| |
| document.addEventListener('DOMContentLoaded', init); |
| </script> |
| </body> |
| </html> |
|
|