Toxicity-Lens / app.py
prann-ab's picture
Rename to Toxicity-Lens; ORT_THREADS option
637b2b2 verified
Raw History Blame Contribute Delete
4.98 kB
import json
import os
import gradio as gr
import uvicorn
from server import ATTRS, Scorer, create_app
# The 8-bit ONNX export on CPU, about 20 ms per comment. The private staging copy of this Space carries the model files in model/;
# the public Space loads them from the model repo.
MODEL = "model" if os.path.isdir("model") else os.environ.get("MODEL_DIR", "abacusai/Toxicity-Lens")
scorer = Scorer(MODEL, backend="onnx")
NAMES = {"TOXICITY": "Toxicity", "SEVERE_TOXICITY": "Severe toxicity", "IDENTITY_ATTACK": "Identity attack", "INSULT": "Insult",
"PROFANITY": "Profanity", "THREAT": "Threat", "SEXUALLY_EXPLICIT": "Sexually explicit"}
EXAMPLES = json.load(open(os.path.join(os.path.dirname(__file__), "examples.json")))
URL = "https://abacusai-toxicity-lens.hf.space"
def score(text: str) -> dict:
"""Score a text on the seven Perspective API attributes (TOXICITY, SEVERE_TOXICITY, IDENTITY_ATTACK, INSULT, PROFANITY, THREAT,
SEXUALLY_EXPLICIT) as probabilities between 0 and 1, on the same scale as the Perspective API.
Works in any language. Texts longer than 256 tokens are scored in overlapping windows and each attribute keeps its highest score.
Args:
text: The text to score, up to 20,480 bytes (the Perspective API's limit).
"""
text = (text or "").strip()
if not text:
raise gr.Error("Type or paste some text first.")
if len(text.encode()) > 20480:
raise gr.Error("Please keep the text under 20,480 bytes, the Perspective API's limit.")
return {a: round(p, 4) for a, p in scorer([text])[0][0].items()}
def show(text: str, threshold: float):
s = score(text)
hits = [NAMES[a] for a in ATTRS if s[a] >= threshold]
verdict = f"**Flagged at {threshold:.2f}:** {', '.join(hits)}" if hits else f"Nothing reaches {threshold:.2f}."
return {NAMES[a]: s[a] for a in ATTRS}, verdict
with gr.Blocks(title="Toxicity-Lens") as demo:
gr.Markdown(
"# Toxicity-Lens\n"
"Scores text in any language on the seven attributes of Google's Perspective API (toxicity, severe toxicity, identity attack, "
"insult, profanity, threat, sexually explicit) with [Toxicity-Lens](https://huggingface.co/abacusai/Toxicity-Lens), "
"an open Apache-2.0 model from Abacus.AI that runs on a CPU. The Perspective API shuts down on December 31, 2026; this Space also "
"answers Perspective API requests (below), so you can try the switch by changing one URL."
)
with gr.Row():
with gr.Column():
inp = gr.Textbox(label="Text, any language", lines=8, max_lines=20, placeholder="Paste a comment, a post or a web page")
thr = gr.Slider(0.5, 0.95, value=0.7, step=0.05, label="Flag threshold")
btn = gr.Button("Score", variant="primary")
with gr.Column():
out = gr.Label(label="Probabilities (the Perspective API's 0 to 1 scale)", num_top_classes=7)
verdict = gr.Markdown()
gr.Examples(examples=[[e] for e in EXAMPLES], inputs=inp, label="Examples")
btn.click(show, inputs=[inp, thr], outputs=[out, verdict], api_visibility="private")
inp.submit(show, inputs=[inp, thr], outputs=[out, verdict], api_visibility="private")
gr.api(score, api_name="score")
gr.Markdown(
"### Compatible with the Perspective API\n"
"This Space serves the Perspective API's `comments:analyze` request and response JSON at the same path:\n"
f"```bash\ncurl -s {URL}/v1alpha1/comments:analyze -H 'Content-Type: application/json' \\\n"
" -d '{\"comment\": {\"text\": \"You are an idiot\"}, \"requestedAttributes\": {\"TOXICITY\": {}, \"INSULT\": {}}}'\n```\n"
"Google's client library needs only a new discovery URL:\n"
"```python\nfrom googleapiclient import discovery\n\n"
"client = discovery.build(\"commentanalyzer\", \"v1alpha1\", developerKey=\"unused\", static_discovery=False,\n"
f" discoveryServiceUrl=\"{URL}/$discovery/rest?version=v1alpha1\")\n"
"body = {\"comment\": {\"text\": \"You are an idiot\"}, \"requestedAttributes\": {\"TOXICITY\": {}}}\n"
"print(client.comments().analyze(body=body).execute()[\"attributeScores\"][\"TOXICITY\"][\"summaryScore\"][\"value\"])\n```\n"
"This shared demo has no rate guarantees. For production, run the same server on your own CPU or GPU in one command; see the "
"[model card](https://huggingface.co/abacusai/Toxicity-Lens). The `score` endpoint is also available to agents as an MCP tool."
)
# the Perspective-compatible routes (server.py) first, the Gradio app and its MCP server mounted behind them. ssr_mode=False: on
# Spaces, server-side rendering makes mount_gradio_app start a Node server that takes port 7860 before uvicorn binds it.
app = gr.mount_gradio_app(create_app(scorer), demo, path="/", mcp_server=True, ssr_mode=False)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=7860)