Spaces:
Running on Zero
Running on Zero
| """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"<sub>Backend: `{get_backend_name()}`</sub>") | |
| 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() | |
| ) | |