Spaces:
Running on Zero
Running on Zero
File size: 5,342 Bytes
aa00509 af9809a aa00509 af9809a aa00509 af9809a aa00509 af9809a aa00509 af9809a 9dee630 af9809a 9dee630 af9809a aa00509 af9809a 9dee630 af9809a 9dee630 af9809a 9dee630 af9809a ff18b7e af9809a aa00509 af9809a 8e5e42f aa00509 af9809a aa00509 9dee630 af9809a aa00509 af9809a d40ce7b 8e5e42f af9809a | 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 | """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()
)
|