TemperCheck / app.py
Joseph Antolick
Guard against paste-time perpetual-resize loop
9dee630
Raw
History Blame Contribute Delete
5.34 kB
"""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()
)