File size: 24,072 Bytes
0229606
1459377
 
 
 
4f3bc5f
a914f33
d09b963
b5805a0
 
1459377
d09b963
1459377
 
4b89faf
d09b963
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b89faf
b5805a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b0c1c3
1459377
7b0c1c3
1459377
 
 
 
 
7b0c1c3
1459377
 
7b0c1c3
d183210
7b0c1c3
 
 
d183210
 
7b0c1c3
 
d183210
 
 
7b0c1c3
 
d183210
 
7b0c1c3
d183210
7b0c1c3
 
 
 
 
d183210
 
7b0c1c3
 
 
 
 
 
d183210
 
7b0c1c3
 
 
 
d183210
7b0c1c3
d183210
7b0c1c3
 
 
 
 
 
 
d183210
 
7b0c1c3
 
 
 
 
d183210
 
 
 
7b0c1c3
 
 
 
 
 
 
 
 
d183210
7b0c1c3
d183210
 
7b0c1c3
d183210
 
 
7b0c1c3
 
 
 
 
 
 
 
 
 
d183210
 
7b0c1c3
 
 
d183210
50021a1
7b0c1c3
 
 
 
 
 
d183210
 
7b0c1c3
 
 
 
 
 
d183210
 
7b0c1c3
 
 
 
 
d183210
b5805a0
 
 
 
 
 
 
 
 
 
 
1459377
 
 
7b0c1c3
d58a6b4
 
 
 
 
 
 
7b0c1c3
 
d58a6b4
 
 
 
7b0c1c3
 
1459377
7b0c1c3
1459377
b5805a0
 
1459377
7b0c1c3
d183210
 
7b0c1c3
 
 
d183210
 
56bfe80
7b0c1c3
 
 
 
 
 
77a7159
 
7b0c1c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cf6094
7b0c1c3
 
 
 
 
 
3cf6094
7b0c1c3
 
 
 
 
 
3cf6094
7b0c1c3
 
 
c0b25cc
7b0c1c3
c0b25cc
 
 
 
 
 
7b0c1c3
 
 
 
 
 
 
b5805a0
 
 
7b0c1c3
 
 
 
d09b963
 
 
 
7b0c1c3
 
b5805a0
 
 
 
 
 
 
7b0c1c3
 
 
 
 
 
 
 
c0b25cc
7b0c1c3
c0b25cc
 
 
7b0c1c3
 
 
3cf6094
7b0c1c3
 
 
 
 
3cf6094
7b0c1c3
 
 
d09b963
 
 
 
7b0c1c3
d09b963
 
7b0c1c3
 
 
3cf6094
7b0c1c3
 
3cf6094
d09b963
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b0c1c3
 
d09b963
 
7b0c1c3
c0b25cc
d09b963
7b0c1c3
 
d09b963
 
a914f33
7b0c1c3
 
 
d09b963
7b0c1c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9f499d
7b0c1c3
 
 
 
 
 
f9f499d
d183210
7b0c1c3
 
 
 
d183210
7b0c1c3
 
 
 
 
3cf6094
7b0c1c3
 
 
 
 
 
 
3cf6094
7b0c1c3
 
 
 
3cf6094
7b0c1c3
 
 
 
3cf6094
7b0c1c3
 
 
 
 
 
 
fd6554d
7b0c1c3
45ffbdc
c0b25cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b0c1c3
 
 
 
 
45ffbdc
7b0c1c3
 
 
d20feeb
7b0c1c3
d20feeb
7b0c1c3
 
 
3cf6094
7b0c1c3
 
d09b963
7b0c1c3
3cf6094
c0b25cc
7b0c1c3
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
import streamlit as st
import cv2
import numpy as np
import os
import time
import threading
import queue
import re
import glob
from datetime import datetime, timezone
from io import BytesIO
from PIL import Image, UnidentifiedImageError
from pipeline import LocalAIEnhancerPipeline

project_dir = os.path.dirname(os.path.abspath(__file__))
MAX_UPLOAD_BYTES = 15 * 1024 * 1024
MAX_IMAGE_PIXELS = 16_000_000


def load_uploaded_image(uploaded_file):
    """Validate upload limits before handing image bytes to OpenCV."""
    file_data = uploaded_file.getvalue()
    if len(file_data) > MAX_UPLOAD_BYTES:
        raise ValueError("Image is too large. Please upload a file smaller than 15 MB.")

    try:
        with Image.open(BytesIO(file_data)) as image:
            width, height = image.size
            if width * height > MAX_IMAGE_PIXELS:
                raise ValueError(
                    "Image resolution is too large. Please upload an image up to 16 megapixels."
                )
            image.verify()
    except (UnidentifiedImageError, OSError, Image.DecompressionBombError) as error:
        raise ValueError("Could not decode image file. Please upload a valid portrait image.") from error

    decoded_image = cv2.imdecode(np.frombuffer(file_data, dtype=np.uint8), cv2.IMREAD_COLOR)
    if decoded_image is None:
        raise ValueError("Could not decode image file. Please upload a valid portrait image.")
    if decoded_image.shape[0] * decoded_image.shape[1] > MAX_IMAGE_PIXELS:
        raise ValueError("Image resolution is too large. Please upload an image up to 16 megapixels.")
    return decoded_image


def enhanced_filename(original_name):
    """Produce a download-safe PNG filename without trusting the upload path."""
    base_name = os.path.basename(original_name.replace("\\", "/"))
    stem, _ = os.path.splitext(base_name)
    safe_stem = re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("._")
    return f"enhanced_{safe_stem or 'portrait'}.png"


TRAIN_LOG_GLOB = os.path.join(project_dir, "models", "CodeFormer", "experiments", "*_CodeFormer_stage3_custom", "train_*.log")
TRAIN_STATE_GLOB = os.path.join(
    project_dir, "models", "CodeFormer", "experiments", "*_CodeFormer_stage3_custom", "training_states", "*.state"
)
TRAIN_TOTAL_ITERATIONS = 20_000


@st.cache_data(ttl=10, show_spinner=False)
def get_training_status(log_glob, state_glob):
    """Read only the recent training log; safe to call during a live CPU run."""
    records = []
    log_mtime = None
    try:
        log_paths = glob.glob(log_glob)
        if not log_paths:
            return records, log_mtime, (None, None)
        log_path = max(log_paths, key=os.path.getmtime)
        log_mtime = os.path.getmtime(log_path)
        with open(log_path, "rb") as handle:
            handle.seek(0, os.SEEK_END)
            handle.seek(max(0, handle.tell() - 350_000))
            text = handle.read().decode("utf-8", errors="replace")
        pattern = re.compile(
            r"iter:\s*([\d,]+).*?eta:\s*(.*?),\s*time.*?"
            r"l_g_pix:\s*([\deE+\-.]+).*?l_g_percep:\s*([\deE+\-.]+).*?"
            r"l_g_identity:\s*([\deE+\-.]+)"
        )
        for match in pattern.finditer(text):
            records.append({
                "iteration": int(match.group(1).replace(",", "")),
                "eta": match.group(2).strip(),
                "pixel_loss": float(match.group(3)),
                "perceptual_loss": float(match.group(4)),
                "identity_loss": float(match.group(5)),
            })
    except OSError:
        pass

    checkpoints = []
    for path in glob.glob(state_glob):
        try:
            checkpoints.append((int(os.path.splitext(os.path.basename(path))[0]), os.path.getmtime(path)))
        except (OSError, ValueError):
            continue
    latest_checkpoint = max(checkpoints, default=(None, None), key=lambda value: value[0])
    return records[-100:], log_mtime, latest_checkpoint


def render_training_dashboard():
    records, log_mtime, latest_checkpoint = get_training_status(TRAIN_LOG_GLOB, TRAIN_STATE_GLOB)
    latest = records[-1] if records else None
    is_running = bool(log_mtime and (datetime.now(timezone.utc).timestamp() - log_mtime < 180))

    st.markdown("<div class='training-panel'><div class='training-kicker'>LIVE TRAINING CONTROL ROOM</div><h2>CodeFormer CPU Training</h2><p>Read-only monitor — viewing this dashboard does not pause or compete with training.</p></div>", unsafe_allow_html=True)
    if st.button("↻ Refresh training data", key="refresh_training"):
        get_training_status.clear()
        st.rerun()

    if not latest:
        st.info("No training metrics found yet. Start training or refresh after the first iteration.")
        return

    progress = min(latest["iteration"] / TRAIN_TOTAL_ITERATIONS, 1.0)
    st.progress(progress, text=f"Iteration {latest['iteration']:,} / {TRAIN_TOTAL_ITERATIONS:,} ({progress:.1%})")
    c1, c2, c3, c4 = st.columns(4)
    status_label = "● RUNNING" if is_running else "● PAUSED / STOPPED"
    status_color = "#34d399" if is_running else "#fbbf24"
    with c1:
        st.markdown(f"<div class='metric-badge'><div class='metric-label'>Status</div><div class='metric-val' style='color:{status_color}'>{status_label}</div></div>", unsafe_allow_html=True)
    with c2:
        st.markdown(f"<div class='metric-badge'><div class='metric-label'>ETA</div><div class='metric-val'>{latest['eta']}</div></div>", unsafe_allow_html=True)
    with c3:
        checkpoint = latest_checkpoint[0] if latest_checkpoint[0] is not None else "—"
        st.markdown(f"<div class='metric-badge'><div class='metric-label'>Last Checkpoint</div><div class='metric-val'>{checkpoint}</div></div>", unsafe_allow_html=True)
    with c4:
        st.markdown(f"<div class='metric-badge'><div class='metric-label'>Identity Loss</div><div class='metric-val'>{latest['identity_loss']:.4f}</div></div>", unsafe_allow_html=True)

    chart_data = [{"iteration": row["iteration"], "Pixel loss": row["pixel_loss"], "Perceptual loss": row["perceptual_loss"], "Identity loss": row["identity_loss"]} for row in records]
    st.markdown("#### Loss trends (latest 100 iterations)")
    st.line_chart(chart_data, x="iteration", y=["Pixel loss", "Perceptual loss", "Identity loss"], color=["#a78bfa", "#f472b6", "#34d399"], height=260)

# ── Page Configuration ──────────────────────────────────────────────────────────
st.set_page_config(
    page_title="AI Portrait Enhancer",
    page_icon="✨",
    layout="wide",
    initial_sidebar_state="expanded"
)

# ── Minimalist Premium Styling ──────────────────────────────────────────────────
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap');

html, body, [class*="css"] {
    font-family: 'Plus Jakarta Sans', sans-serif !important;
}

.stApp {
    background: radial-gradient(ellipse 100% 80% at 50% -20%, rgba(124, 58, 237, 0.15) 0%, #090714 80%);
    color: #f3f0ff;
}

section[data-testid="stSidebar"] {
    background: #0d0a1d !important;
    border-right: 1px solid rgba(255, 255, 255, 0.08) !important;
}

.brand-header {
    background: linear-gradient(135deg, #7c3aed 0%, #db2777 100%);
    padding: 24px 20px;
    border-radius: 16px;
    text-align: center;
    margin-bottom: 24px;
    box-shadow: 0 8px 24px rgba(124, 58, 237, 0.25);
}

.brand-header h2 {
    color: white;
    font-weight: 800;
    font-size: 1.25rem;
    margin: 0;
    letter-spacing: 0.5px;
}

.brand-header p {
    color: rgba(255, 255, 255, 0.85);
    font-size: 0.78rem;
    margin: 4px 0 0 0;
}

.hero-title {
    font-size: 2.8rem;
    font-weight: 800;
    background: linear-gradient(135deg, #ffffff 0%, #c4b5fd 50%, #f472b6 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    text-align: center;
    margin-bottom: 6px;
}

.hero-sub {
    font-size: 1.05rem;
    color: #94a3b8;
    text-align: center;
    margin-bottom: 32px;
}

div.stButton > button {
    background: linear-gradient(135deg, #7c3aed 0%, #db2777 100%);
    color: white !important;
    border: none;
    border-radius: 12px;
    padding: 14px 28px;
    font-weight: 700;
    font-size: 1rem;
    width: 100%;
    transition: all 0.2s ease;
    box-shadow: 0 4px 20px rgba(124, 58, 237, 0.3);
}

div.stButton > button:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 28px rgba(124, 58, 237, 0.5);
}

div.stDownloadButton > button {
    background: linear-gradient(135deg, #059669 0%, #10b981 100%);
    color: white !important;
    border: none;
    border-radius: 12px;
    padding: 14px 28px;
    font-weight: 700;
    font-size: 1rem;
    width: 100%;
    transition: all 0.2s ease;
    box-shadow: 0 4px 20px rgba(16, 185, 129, 0.3);
}

div.stDownloadButton > button:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 28px rgba(16, 185, 129, 0.5);
}

.metric-badge {
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 12px;
    padding: 14px 18px;
    text-align: center;
}

.metric-label {
    font-size: 0.75rem;
    color: #94a3b8;
    text-transform: uppercase;
    font-weight: 600;
    letter-spacing: 0.5px;
}

.metric-val {
    font-size: 1.1rem;
    color: #f3f0ff;
    font-weight: 700;
    margin-top: 4px;
}

.training-panel {
    margin: 14px 0 20px;
    padding: 22px 24px;
    border: 1px solid rgba(167, 139, 250, 0.3);
    border-radius: 18px;
    background: linear-gradient(120deg, rgba(124, 58, 237, 0.20), rgba(219, 39, 119, 0.10));
}
.training-panel h2 { margin: 4px 0; color: #fff; font-size: 1.45rem; }
.training-panel p { margin: 0; color: #c4b5fd; }
.training-kicker { color: #f9a8d4; font-size: .72rem; font-weight: 800; letter-spacing: .12em; }
</style>
""", unsafe_allow_html=True)

# ── Session State Initializer Guard ─────────────────────────────────────────────
for key, default in [
    ('processing', False),
    ('enhanced_img', None),
    ('processing_error', None),
    ('process_duration', None),
    ('start_time', None),
    ('last_run_params', None),
    ('progress_state', None),
    ('num_faces_detected', 0)
]:
    if key not in st.session_state:
        st.session_state[key] = default

# ── Load Pipeline Resource ──────────────────────────────────────────────────────
@st.cache_resource(show_spinner=False)
def get_pipeline():
    return LocalAIEnhancerPipeline()

# The dashboard must not initialise the heavy model while CPU training is live.
pipeline = None

# ── Sidebar Controls (Minimalist & Clean) ───────────────────────────────────────
with st.sidebar:
    st.markdown("""
    <div class="brand-header">
        <h2>✨ Wink Studio</h2>
        <p>AI Portrait & Image Restoration</p>
    </div>
    """, unsafe_allow_html=True)

    # Preset selection
    preset_choice = st.radio(
        "Enhancement Preset",
        ["✨ Wink Studio (Best Quality)", "⚡ Ultra Fast CPU", "🎨 Natural Likeness"],
        index=0,
        help="Select pre-configured quality mode."
    )

    st.markdown("<br>", unsafe_allow_html=True)

    # Core 3 Controls
    if "Wink Studio" in preset_choice:
        default_w = 0.3
        default_upscale = 2
        default_wink = True
        default_grain = 0.15
        default_color = True
        default_eye = True
        default_detector = "retinaface_mobile0.25"
    elif "Ultra Fast" in preset_choice:
        default_w = 0.5
        default_upscale = 1
        default_wink = False
        default_grain = 0.0
        default_color = False
        default_eye = False
        default_detector = "retinaface_mobile0.25"
    else: # Natural Likeness
        default_w = 0.65
        default_upscale = 2
        default_wink = True
        default_grain = 0.1
        default_color = True
        default_eye = True
        default_detector = "retinaface_mobile0.25"

    w_val = st.slider(
        "AI Detail vs Likeness (w)",
        min_value=0.0,
        max_value=1.0,
        value=default_w,
        step=0.05,
        help="0.0 = Max AI Detail restoration. 1.0 = Keep exact original face likeness."
    )

    upscale_val = st.select_slider(
        "Output Resolution Scale",
        options=[1, 2, 4],
        value=default_upscale,
        format_func=lambda x: f"{x}× Resolution"
    )

    # Advanced Settings (Collapsible to keep UI clean)
    with st.expander("⚙️ Advanced Tuning", expanded=False):
        face_detector = st.selectbox(
            "Detector Model",
            ["retinaface_mobile0.25", "retinaface_resnet50", "YOLOv5n", "YOLOv5l"],
            index=0
        )
        det_thresh = st.slider("Detection Threshold", 0.1, 1.0, 0.5, 0.05)
        wink_mode = st.toggle("Wink Quality Engine", value=default_wink)
        skin_grain = st.slider("Skin Grain Retention", 0.0, 0.5, default_grain, 0.05)
        sharpen_val = st.slider("🔥 Extra Sharpness Boost", 0.0, 1.0, 0.2, 0.05, help="Multi-scale edge-aware adaptive sharpening")
        color_match = st.checkbox("Auto Skin Tone Alignment", value=default_color)
        
        st.markdown("**🎭 Facial Organ Enhancements**")
        enable_eyes = st.checkbox("👁️ Eye Sparkle & Contrast Boost", value=default_eye)
        enable_lips = st.checkbox("👄 Lip Saturation & Definition", value=True)
        enable_skin = st.checkbox("💆 Real Skin Grain Retention", value=True)

        bg_upscale = st.toggle("Real-ESRGAN Background Upscale", value=False)
        face_upscale = st.toggle("Real-ESRGAN Face Upscale", value=False)

# ── Main Header ─────────────────────────────────────────────────────────────────
st.markdown('<div class="hero-title">AI Portrait Enhancer</div>', unsafe_allow_html=True)
st.markdown('<div class="hero-sub">Restore blurry portraits, skin texture & eye detail with studio-level clarity</div>', unsafe_allow_html=True)

with st.expander("📈 Training Dashboard", expanded=True):
    render_training_dashboard()

# ── File Upload Section ────────────────────────────────────────────────────────
uploaded_file = st.file_uploader("Upload portrait photo (PNG, JPG, WEBP)", type=["png", "jpg", "jpeg", "webp"])

if uploaded_file is not None:
    try:
        input_img = load_uploaded_image(uploaded_file)
    except ValueError as error:
        st.error(str(error))
        st.stop()

    if pipeline is None:
        try:
            pipeline = get_pipeline()
        except Exception as error:
            st.error(f"Failed to initialize AI Pipeline: {error}")
            st.stop()

    current_params = {
        'img_name': uploaded_file.name,
        'w': w_val,
        'upscale': upscale_val,
        'detector': face_detector,
        'thresh': det_thresh,
        'wink': wink_mode,
        'grain': skin_grain,
        'sharpen': sharpen_val,
        'color': color_match,
        'eye': enable_eyes,
        'lip': enable_lips,
        'skin': enable_skin,
        'bg_up': bg_upscale,
        'face_up': face_upscale
    }

    # Parameters change guard: reset output state if parameters change while idle
    if st.session_state.get('last_run_params') != current_params and not st.session_state.get('processing'):
        st.session_state.enhanced_img = None
        st.session_state.processing_error = None
        st.session_state.process_duration = None

    # Trigger processing thread if output is None
    if st.session_state.enhanced_img is None and st.session_state.get('processing_error') is None:
        if not st.session_state.get('processing'):
            if pipeline is None:
                st.session_state.processing_error = "AI pipeline is unavailable. Please try again later."
                st.rerun()

            st.session_state.processing = True
            request_start_time = time.time()
            st.session_state.start_time = request_start_time
            
            res_queue = queue.Queue()
            st.session_state._result_queue = res_queue

            def local_progress_callback(stage, progress, message):
                res_queue.put({'type': 'progress', 'stage': stage, 'progress': progress, 'message': message})

            process_args = {
                'w': w_val,
                'detection_model': face_detector,
                'upscale': upscale_val,
                'blend_softness': 0.5,
                'bg_upsampler': 'realesrgan' if bg_upscale else None,
                'det_threshold': det_thresh,
                'sharpen_amount': sharpen_val,
                'face_upsample': face_upscale,
                'parallel': True,
                'wink_mode': wink_mode,
                'eye_enhancement': enable_eyes,
                'skin_grain': skin_grain,
                'color_match': color_match,
                'enable_eyes': enable_eyes,
                'enable_lips': enable_lips,
                'enable_skin': enable_skin,
                'progress_callback': local_progress_callback,
            }

            def _worker(
                request_image=input_img.copy(),
                request_params=current_params.copy(),
                request_args=process_args.copy(),
                request_queue=res_queue,
                request_started_at=request_start_time,
            ):
                try:
                    res = pipeline.process_image(
                        request_image,
                        **request_args,
                    )

                    request_queue.put({
                        'type': 'result',
                        'enhanced_img': res,
                        'duration': time.time() - request_started_at,
                        'params': request_params
                    })
                except Exception as ex:
                    import traceback
                    traceback.print_exc()
                    request_queue.put({'type': 'error', 'error': str(ex)})

            threading.Thread(target=_worker, daemon=True).start()

    # Poll Queue for updates
    if st.session_state.get('processing'):
        res_queue = st.session_state.get('_result_queue')
        if res_queue:
            while not res_queue.empty():
                msg = res_queue.get_nowait()
                if msg['type'] == 'progress':
                    st.session_state.progress_state = msg
                elif msg['type'] == 'result':
                    st.session_state.enhanced_img = msg['enhanced_img']
                    st.session_state.process_duration = msg['duration']
                    st.session_state.last_run_params = msg['params']
                    st.session_state.processing = False
                    st.session_state.progress_state = None
                    st.rerun()
                elif msg['type'] == 'error':
                    st.session_state.processing_error = msg['error']
                    st.session_state.processing = False
                    st.session_state.progress_state = None
                    st.rerun()

        # Render Progress UI
        p_state = st.session_state.get('progress_state') or {}
        stage_msg = p_state.get('message', 'Processing image with AI...')
        prog_val = p_state.get('progress', 0.1)

        st.markdown("<br>", unsafe_allow_html=True)
        st.progress(float(prog_val))
        st.info(f"✨ {stage_msg}")
        time.sleep(0.3)
        st.rerun()

    # Render Errors if any
    if st.session_state.get('processing_error'):
        st.error(f"Processing Error: {st.session_state.processing_error}")
        if st.button("🔄 Try Again"):
            st.session_state.processing_error = None
            st.session_state.processing = False
            st.rerun()

    # Render Results Section
    enhanced_img = st.session_state.get('enhanced_img')
    if enhanced_img is not None:
        st.markdown("<hr>", unsafe_allow_html=True)

        # Image Stats Bar
        in_h, in_w = input_img.shape[:2]
        out_h, out_w = enhanced_img.shape[:2]
        duration = st.session_state.get('process_duration', 0.0)

        col1, col2, col3 = st.columns(3)
        with col1:
            st.markdown(f'<div class="metric-badge"><div class="metric-label">Original Size</div><div class="metric-val">{in_w}×{in_h} px</div></div>', unsafe_allow_html=True)
        with col2:
            st.markdown(f'<div class="metric-badge"><div class="metric-label">Enhanced Size</div><div class="metric-val">{out_w}×{out_h} px</div></div>', unsafe_allow_html=True)
        with col3:
            st.markdown(f'<div class="metric-badge"><div class="metric-label">Speed (CPU)</div><div class="metric-val">{duration:.2f} s</div></div>', unsafe_allow_html=True)

        st.markdown("<br>", unsafe_allow_html=True)

        # AI Quality Score Report Card
        if pipeline and hasattr(pipeline, 'wink_enhancer'):
            q_report = pipeline.wink_enhancer.calculate_quality_report(input_img, enhanced_img)
            st.markdown("#### 📊 AI Quality Score Report")
            q1, q2, q3, q4 = st.columns(4)
            with q1:
                st.markdown(f'<div class="metric-badge"><div class="metric-label">Sharpness Gain</div><div class="metric-val" style="color: #34d399;">+{q_report["sharpness_gain_pct"]}%</div></div>', unsafe_allow_html=True)
            with q2:
                st.markdown(f'<div class="metric-badge"><div class="metric-label">Original Sharpness</div><div class="metric-val">{q_report["orig_sharpness"]}</div></div>', unsafe_allow_html=True)
            with q3:
                st.markdown(f'<div class="metric-badge"><div class="metric-label">Enhanced Sharpness</div><div class="metric-val">{q_report["enh_sharpness"]}</div></div>', unsafe_allow_html=True)
            with q4:
                st.markdown(f'<div class="metric-badge"><div class="metric-label">Skin Tone Match</div><div class="metric-val" style="color: #60a5fa;">{q_report["tone_fidelity_pct"]}%</div></div>', unsafe_allow_html=True)

        st.markdown("<br>", unsafe_allow_html=True)

        # Side-by-Side Comparison Display
        c_orig, c_enh = st.columns(2)
        with c_orig:
            st.markdown("##### 📷 Original Image")
            st.image(cv2.cvtColor(input_img, cv2.COLOR_BGR2RGB), use_container_width=True)

        with c_enh:
            st.markdown("##### ✨ Wink Enhanced HD")
            st.image(cv2.cvtColor(enhanced_img, cv2.COLOR_BGR2RGB), use_container_width=True)

        st.markdown("<br>", unsafe_allow_html=True)

        # Download Button
        success, encoded_buf = cv2.imencode('.png', enhanced_img)
        if success:
            st.download_button(
                label="⬇️ Download Enhanced HD Image",
                data=encoded_buf.tobytes(),
                file_name=enhanced_filename(uploaded_file.name),
                mime="image/png"
            )

else:
    # Empty State Guide
    st.markdown("""
    <div style="text-align: center; padding: 60px 20px; border: 2px dashed rgba(255,255,255,0.1); border-radius: 20px; background: rgba(255,255,255,0.01);">
        <p style="font-size: 1.2rem; color: #94a3b8; font-weight: 600;">Drag and drop any portrait photo above to get started</p>
        <p style="font-size: 0.9rem; color: #64748b; margin-top: 8px;">Supports PNG, JPG, JPEG, WEBP. Optimized for fast CPU execution.</p>
    </div>
    """, unsafe_allow_html=True)