""" Pour Judgement — drop a photo of your latte art, face the judge. Gradio app for the Build Small Hackathon (Thousand Token Wood track). Local-only inference: OpenCV metrics + a small VLM via llama.cpp. """ from __future__ import annotations import os # ------------------------ SSL fix: clear cert env vars pointing to missing files # (must run before importing gradio/httpx — they read these at import time) for _var in ("SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"): _p = os.environ.get(_var, "") if _p and not os.path.exists(_p): print(f"[app] Clearing {_var} -> file not found: {_p}") os.environ.pop(_var, None) import gradio as gr from judge import JUDGE_NAME, JUDGE_TITLE, judge from scoring import score_image # ----------------------------------------------------------------- UI pieces CSS = """ @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400..900;1,9..144,400..700&family=Inter:wght@400;600&display=swap'); :root, .gradio-container { --espresso: #241712; --roast: #3a2a20; --crema: #e9c9a0; --milk: #faf4eb; --ink: #2a1e18; --stamp: #8c3b2e; } .gradio-container { background: var(--espresso) !important; font-family: 'Inter', sans-serif !important; max-width: 860px !important; margin: 0 auto !important; } #title h1 { font-family: 'Fraunces', serif !important; font-weight: 900; font-size: 2.6rem; letter-spacing: -0.02em; color: var(--crema); text-align: center; margin-bottom: 0; } #subtitle p { color: #b59d85; text-align: center; font-size: 0.95rem; margin-top: 0.25rem; } #drop .image-container, #drop { border: 2px dashed #6b5443 !important; border-radius: 14px !important; background: var(--roast) !important; } #go { background: var(--crema) !important; color: var(--ink) !important; font-family: 'Fraunces', serif !important; font-weight: 700 !important; font-size: 1.15rem !important; border: none !important; border-radius: 10px !important; } #go:hover { background: #f3dcbb !important; } /* The scorecard — signature element. Colors are hard-pinned with !important because Gradio's dark theme injects white text into HTML-component children, which made the tip unreadable. */ .scorecard { background: var(--milk) !important; color: #2a1e18 !important; border-radius: 6px; padding: 28px 30px; position: relative; box-shadow: 0 12px 40px rgba(0,0,0,0.45); font-family: 'Inter', sans-serif; } .scorecard * { color: #2a1e18 !important; } .scorecard .header { font-family: 'Fraunces', serif; font-size: 0.8rem; letter-spacing: 0.18em; text-transform: uppercase; color: #7a6552 !important; border-bottom: 1px solid #d9c8b4; padding-bottom: 10px; margin-bottom: 16px; } .scorecard .pattern { font-family: 'Fraunces', serif; font-style: italic; font-weight: 600; font-size: 1.5rem; line-height: 1.25; margin-bottom: 14px; } .scorecard .verdict { font-size: 1rem; line-height: 1.55; margin-bottom: 18px; } .scorecard .tip { background: #f1e4d2 !important; border-left: 4px solid #c89a64; padding: 12px 14px; border-radius: 0 8px 8px 0; font-size: 0.95rem; } .scorecard .tip, .scorecard .tip b { color: #4a3526 !important; } .scorecard .tip b { font-family: 'Fraunces', serif; } .stamp { position: absolute; top: 18px; right: 22px; transform: rotate(8deg); border: 3px solid var(--stamp); color: var(--stamp) !important; border-radius: 8px; font-family: 'Fraunces', serif; font-weight: 900; font-size: 1.9rem; padding: 6px 14px; opacity: 0.85; } .bars { margin-top: 18px; } .bar-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; } .bar-label { width: 112px; font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; color: #7a6552 !important; } .bar-track { flex: 1; height: 10px; background: #e3d3bd !important; border-radius: 5px; overflow: hidden; } .bar-fill { height: 100%; background: linear-gradient(90deg, #c89a64, #8c5a33) !important; border-radius: 5px; } .bar-num { width: 38px; text-align: right; font-size: 0.82rem; font-weight: 600; color: #2a1e18 !important; } .judging { color: var(--crema); font-family: 'Fraunces', serif; font-style: italic; text-align: center; font-size: 1.05rem; } footer { display: none !important; } """ JUDGING_LINES = "The judge lifts the cup to the light… swirls… inhales… deliberates…" BAR_LABELS = {"texture": "milk texture", "flow": "flow & structure"} def render_card(result: dict, verdict: dict) -> str: s = result["subscores"] bars = "".join( f"""
""" for name, val in s.items() ) return f"""