"""TemperCheck — Gradio app. Upload a social-media profile screenshot; get a playful "temper" read from a small Gemma 4 vision model. For the Build Small Hackathon (model <= 32B, Gradio on Hugging Face Spaces). Run locally (Ollama backend, default): uv run app.py Run the Spaces backend locally: TEMPER_BACKEND=transformers uv run app.py """ from __future__ import annotations import os import gradio as gr from tempercheck import get_backend_name, score_image PORT = int(os.environ.get("TEMPER_PORT", "7140")) # Bind real Ctrl-V: Gradio's "clipboard" source only adds a click-to-paste # button, so we listen for the browser paste event ourselves and feed the # pasted image into the component's hidden file input. If an image is already # loaded the upload input is gone from the DOM, so we click the clear button # first, then inject on the next tick. # # Guards against a perpetual-resize loop: # * __temperPasteBound — demo.load() can re-run on reconnect, so bind the # paste listener exactly once; stacked listeners inject the image several # times and thrash the layout. # * `busy` lock — ignore further paste events for a moment while one is being # processed, so a single Ctrl-V can't kick off overlapping injects. PASTE_JS = """ () => { if (window.__temperPasteBound) return; window.__temperPasteBound = true; let busy = false; const inject = (blob) => { const root = document.querySelector('#temper_image'); if (!root) return; let input = root.querySelector("input[type='file']"); if (!input) { const clear = root.querySelector("button[aria-label='Clear'], button[title='Clear']"); if (clear) { clear.click(); return setTimeout(() => inject(blob), 80); } return; } const dt = new DataTransfer(); dt.items.add(new File([blob], 'pasted.png', { type: blob.type || 'image/png' })); input.files = dt.files; input.dispatchEvent(new Event('change', { bubbles: true })); }; window.addEventListener('paste', (e) => { if (busy) return; const items = (e.clipboardData || window.clipboardData)?.items || []; for (const it of items) { if (it.type && it.type.startsWith('image/')) { const blob = it.getAsFile(); if (blob) { e.preventDefault(); busy = true; inject(blob); setTimeout(() => { busy = false; }, 600); } return; } } }); } """ DISCLAIMER = ( "TemperCheck is a fast LLM-based back-of-the-envelope calculation to see if a " "given social media profile is run by someone with a bad temper. It performs a " "quick visual analysis of a screenshot of a profile, and makes a hypothetical " "judgment accordingly. *It is NOT a real personality and says nothing factual " "about any actual person.*" ) def _render(verdict) -> tuple[str, str]: bar = "█" * (verdict.score // 5) + "░" * (20 - verdict.score // 5) signals = " ".join(f"`{s}`" for s in verdict.signals) if verdict.signals else "—" md = ( f"## {verdict.band}\n\n" f"**{verdict.verdict}** — temper **{verdict.score}/100**\n\n" f"`{bar}`\n\n" f"{verdict.rationale}\n\n" f"**Signals:** {signals}" ) return md, verdict.raw def analyze(image): if image is None: return "Upload a profile screenshot to get a read.", "" try: return _render(score_image(image)) except Exception as exc: # surface backend/connection errors to the user return ( f"⚠️ Could not score this image.\n\n```\n{type(exc).__name__}: {exc}\n```", "", ) def _disable_button(): return gr.update(interactive=False) def _enable_button(): return gr.update(interactive=True) with gr.Blocks(title="TemperCheck") as demo: gr.Markdown("# 😤 TemperCheck\n### How short a temper does this profile look to have?") gr.Markdown(DISCLAIMER) with gr.Row(): with gr.Column(): image_in = gr.Image( type="pil", label="Profile image / screenshot", sources=["upload", "clipboard"], elem_id="temper_image", height=400, # fixed box stops the paste-time resize feedback loop ) go = gr.Button("Check the temper ☕", variant="primary") with gr.Column(): result = gr.Markdown(label="Verdict") with gr.Accordion("Raw model output (agent trace)", open=False): raw = gr.Code(label="raw") # Only the button runs a check — pasting/uploading just loads the image. # Disable the button while the check runs, then re-enable it; analyze() never # raises, so the final .then always restores the button. go.click(_disable_button, None, go).then( analyze, inputs=image_in, outputs=[result, raw] ).then(_enable_button, None, go) gr.Markdown(f"Backend: `{get_backend_name()}`") demo.load(None, None, None, js=PASTE_JS) if __name__ == "__main__": if os.environ.get("SPACE_ID"): # On Hugging Face Spaces, let the platform manage host/port. demo.launch(theme=gr.themes.Soft()) else: demo.launch( server_name="0.0.0.0", server_port=PORT, theme=gr.themes.Soft() )