ShukGEN / index.html
shukdev3's picture
Update index.html
005a918 verified
Raw
History Blame Contribute Delete
52 kB
<!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>
/* ═══════════════════════════════════════════════════════════
DESIGN TOKENS (mirrors PyQt6 DARK palette)
═══════════════════════════════════════════════════════════ */
: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: ;
}
/* LIGHT THEME */
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;
}
/* ── Layout ── */
#app { display: flex; flex-direction: column; height: 100vh; }
/* ── Title Bar ── */
#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 (sidebar + content) ── */
#body { display: flex; flex: 1; min-height: 0; }
/* ── Sidebar ── */
#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 Headers ── */
.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; }
/* ── Buttons ── */
.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 */
#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 bar */
.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 */
.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 Area ── */
#content { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
/* ── Tabs ── */
.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 strip */
.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); }
/* ── Style Gallery ── */
#gallery-scroll { flex: 1; overflow-y: auto; padding: 16px; }
#gallery-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
/* Style cards */
.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 Tab ── */
#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 Tab ── */
#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;
}
/* ── Latent Walk Tab ── */
#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);
}
/* ── Status Bar ── */
#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 ── */
#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; }
/* ── Scrollbars ── */
::-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 URL bar ── */
.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;
}
/* ── Model path input ── */
.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); }
/* drag-and-drop zone */
#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>
<!-- Lightbox -->
<div id="lightbox" onclick="closeLightbox()">
<img id="lightbox-img" src="" alt="Preview" />
</div>
<div id="app">
<!-- ── Title Bar ── -->
<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>
<!-- ── Body ── -->
<div id="body">
<!-- ══ SIDEBAR ══ -->
<div id="sidebar">
<!-- Server -->
<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>
<!-- Step 1: Load Model -->
<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>
<!-- Step 2: Upload Image -->
<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>
<!-- Step 3: Generate -->
<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>
<!-- Step 4: Export -->
<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>
<!-- Quick Guide -->
<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 &amp; Walk tabs!</p>
</div>
</div><!-- /sidebar -->
<!-- ══ CONTENT ══ -->
<div id="content">
<!-- Tab bar -->
<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>
<!-- ── Tab: Gallery ── -->
<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">
<!-- Cards built by JS -->
</div>
</div>
</div>
<!-- ── Tab: Compare ── -->
<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">
<!-- Original -->
<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>
<!-- Reconstruction -->
<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>
<!-- Styled -->
<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>
<!-- ── Tab: Strength ── -->
<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 &nbsp;|&nbsp; Ξ±=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">
<!-- built by JS -->
</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>
<!-- ── Tab: Latent Walk ── -->
<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><!-- /content -->
</div><!-- /body -->
<!-- ── Status Bar ── -->
<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><!-- /app -->
<!-- About dialog -->
<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>
// ═══════════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════════
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"];
// ═══════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════
const S = {
serverURL: '',
modelLoaded: false,
imageLoaded: false,
generated: false,
origB64: null,
reconB64: null,
styledB64: new Array(8).fill(null),
uploadedImg: null, // File object
};
// ═══════════════════════════════════════════════════════════
// SERVER HELPERS
// ═══════════════════════════════════════════════════════════
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();
}
// ═══════════════════════════════════════════════════════════
// PING / CONNECT
// ═══════════════════════════════════════════════════════════
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';
// Device badge
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.');
}
}
// ═══════════════════════════════════════════════════════════
// LOAD MODEL
// ═══════════════════════════════════════════════════════════
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?');
}
}
// ═══════════════════════════════════════════════════════════
// IMAGE UPLOAD
// ═══════════════════════════════════════════════════════════
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;
// Show preview in sidebar
const box = document.getElementById('input-preview-box');
box.innerHTML = `<img src="${b64}" style="width:100%;height:100%;object-fit:contain;" />`;
// Update compare original
updateCompareOrig(b64);
setStatus('πŸ–Ό Image loaded: ' + file.name);
// Send to server
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);
}
// ═══════════════════════════════════════════════════════════
// GENERATE
// ═══════════════════════════════════════════════════════════
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…');
// Set all cards to loading
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);
// Update gallery cards
for (let i = 0; i < 8; i++) setCardImage(i, data.styles[i]);
// Update compare tab
updateCompareRecon(data.recon);
updateCompareStyled();
// Update strength previews (recon panel)
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?');
}
}
// ═══════════════════════════════════════════════════════════
// STRENGTH PREVIEW
// ═══════════════════════════════════════════════════════════
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;
// Recon always shown
setImgOrPh('sp-recon', S.reconB64);
// Full styled
setImgOrPh('sp-styled', S.styledB64[styleIdx]);
// Blend from server
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;
}
// ═══════════════════════════════════════════════════════════
// LATENT WALK
// ═══════════════════════════════════════════════════════════
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?');
}
}
// ═══════════════════════════════════════════════════════════
// COMPARE TAB
// ═══════════════════════════════════════════════════════════
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;" />`;
}
// ═══════════════════════════════════════════════════════════
// SAVE
// ═══════════════════════════════════════════════════════════
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…');
}
// ═══════════════════════════════════════════════════════════
// LIGHTBOX
// ═══════════════════════════════════════════════════════════
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');
}
// ═══════════════════════════════════════════════════════════
// GALLERY CARDS
// ═══════════════════════════════════════════════════════════
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]);
}
// ═══════════════════════════════════════════════════════════
// COMPARE STYLE SELECT
// ═══════════════════════════════════════════════════════════
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);
});
}
// ═══════════════════════════════════════════════════════════
// STRENGTH RADIO GROUP
// ═══════════════════════════════════════════════════════════
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);
});
}
// ═══════════════════════════════════════════════════════════
// WALK SELECTS
// ═══════════════════════════════════════════════════════════
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);
});
});
}
// ═══════════════════════════════════════════════════════════
// ABOUT
// ═══════════════════════════════════════════════════════════
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.';
});
}
// ═══════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════
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}')" />`;
}
}
// ── Tab switching ──
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');
}
// ── Theme ──
let isDark = true;
function toggleTheme() {
isDark = !isDark;
document.body.classList.toggle('light', !isDark);
document.getElementById('theme-btn').textContent = isDark ? 'β˜€ Light Mode' : 'πŸŒ™ Dark Mode';
}
// ═══════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════
function init() {
buildGallery();
buildCompareSelect();
buildStrengthRadios();
buildWalkSelects();
// Auto-ping server
pingServer();
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>