from __future__ import annotations import os from pathlib import Path from typing import Callable LABELS = { 0: "Normal", 1: "Hate/Offensive", } WEIGHT_FILENAMES = ("model.safetensors", "pytorch_model.bin") TOKENIZER_FILENAMES = ( "tokenizer.json", "tokenizer_config.json", "vocab.txt", "special_tokens_map.json", ) DEFAULT_MODEL_PATH = "/Users/qqq/Downloads/fine_tuned_hatebert_model" LIGHT_KEYNOTE_CSS = """ :root { --paper: #f7fbff; --paper-2: #eef5fb; --paper-3: #e8eef5; --ink: #15202a; --muted: #5f6f7d; --accent: #4f87b3; --accent-soft: #d8e7f4; --clear: #2d7a57; --danger: #b65745; --line: rgba(35, 71, 102, 0.12); } .gradio-container { background: radial-gradient(circle at 86% 14%, rgba(107, 177, 226, 0.18), transparent 18%), linear-gradient(90deg, rgba(35, 71, 102, 0.05) 1px, transparent 1px), linear-gradient(rgba(35, 71, 102, 0.04) 1px, transparent 1px), linear-gradient(150deg, var(--paper) 0%, var(--paper-2) 46%, var(--paper-3) 100%) !important; background-size: auto, 40px 40px, 40px 40px, auto !important; color: var(--ink) !important; font-family: "Aptos", "Segoe UI", sans-serif !important; } .prototype-shell { max-width: 1320px; margin: 0 auto; padding: 18px 18px 28px; } .topbar { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-bottom: 16px; } .brand { display: flex; align-items: center; gap: 12px; } .brand-mark { width: 36px; height: 36px; border-radius: 12px; background: linear-gradient(135deg, #14212b, #36556e); color: #9fd0f7; display: grid; place-items: center; font-weight: 700; font-size: 15px; } .brand-copy { min-width: 0; } .brand-title { font-family: Georgia, "Times New Roman", serif; font-size: 19px; } .brand-subtitle { margin-top: 2px; color: rgba(21, 32, 42, 0.56); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; } .status-badge { padding: 10px 12px; border: 1px solid rgba(35, 71, 102, 0.1); background: rgba(255, 255, 255, 0.52); color: #2e5b82; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; } .prototype-stage { display: grid; grid-template-columns: minmax(0, 1.12fr) minmax(410px, 0.88fr); gap: 24px; align-items: start; } .stage-copy { padding: 10px 0 6px; } .stage-kicker { color: var(--accent); font-size: 12px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; } .stage-title { margin: 14px 0 0; max-width: 9em; font-family: Georgia, "Times New Roman", serif; font-size: clamp(42px, 5.2vw, 68px); line-height: 0.96; letter-spacing: -0.05em; } .stage-subtitle { max-width: 35rem; margin-top: 14px; color: var(--muted); font-size: 15px; line-height: 1.55; } .stage-proof { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 18px; max-width: 700px; } .stage-proof-item { border-top: 1px solid var(--line); padding-top: 12px; } .stage-proof-label { color: var(--muted); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; } .stage-proof-value { margin-top: 6px; font-family: Georgia, "Times New Roman", serif; font-size: 22px; } .stage-chips { margin-top: 14px; display: flex; gap: 8px; flex-wrap: wrap; } .stage-chip { padding: 8px 10px; border-radius: 999px; border: 1px solid rgba(37, 85, 123, 0.1); background: rgba(37, 85, 123, 0.06); color: #25557b; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; } .queue { margin-top: 16px; display: grid; gap: 12px; max-width: 640px; } .queue-card { display: grid; grid-template-columns: 68px 1fr auto; gap: 14px; align-items: center; padding: 14px; border: 1px solid rgba(35, 71, 102, 0.08); background: rgba(255, 255, 255, 0.62); box-shadow: 0 16px 34px rgba(114, 145, 175, 0.08); } .queue-score { font-family: Georgia, "Times New Roman", serif; font-size: 24px; color: #25557b; } .queue-title { font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: rgba(21, 32, 42, 0.52); } .queue-text { margin-top: 4px; color: rgba(21, 32, 42, 0.78); line-height: 1.42; } .queue-pill { padding: 8px 10px; border-radius: 999px; border: 1px solid rgba(37, 85, 123, 0.1); background: rgba(37, 85, 123, 0.06); color: #25557b; font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; } .stage-note { margin-top: 14px; padding: 12px 14px; border-left: 3px solid #5a8db5; background: rgba(255, 255, 255, 0.48); color: rgba(21, 32, 42, 0.76); line-height: 1.5; } .prototype-panel { background: rgba(255, 255, 255, 0.58); border: 1px solid rgba(35, 71, 102, 0.1); box-shadow: 0 22px 50px rgba(96, 132, 166, 0.12); backdrop-filter: blur(16px); padding: 24px; } .panel-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; } .panel-kicker { color: rgba(21, 32, 42, 0.55); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; } .panel-title { margin-top: 8px; font-family: Georgia, "Times New Roman", serif; font-size: 34px; line-height: 0.98; } .panel-badge { padding: 8px 10px; border: 1px solid rgba(79, 135, 179, 0.16); background: rgba(79, 135, 179, 0.08); color: #4076a1; font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; } .prototype-panel textarea, .prototype-panel input, .prototype-panel .gradio-textbox textarea { background: rgba(243, 249, 255, 0.82) !important; border: 1px solid rgba(35, 71, 102, 0.1) !important; color: rgba(21, 32, 42, 0.78) !important; } .prototype-panel label, .prototype-panel .gradio-form label { color: rgba(21, 32, 42, 0.55) !important; font-size: 11px !important; letter-spacing: 0.14em !important; text-transform: uppercase !important; } .prototype-panel button.primary, .prototype-panel button.lg.primary { background: linear-gradient(135deg, #1f4e72, #5a8db5) !important; border: 0 !important; color: white !important; } .prototype-panel button.secondary { border: 1px solid rgba(35, 71, 102, 0.1) !important; background: rgba(255, 255, 255, 0.54) !important; color: #234966 !important; } .prototype-result { margin-top: 22px; padding-top: 22px; border-top: 1px solid rgba(35, 71, 102, 0.1); display: grid; grid-template-columns: 1fr 168px; gap: 16px; align-items: center; } .result-copy { min-width: 0; } .result-kicker { color: rgba(21, 32, 42, 0.52); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; } .result-label { margin-top: 8px; font-family: Georgia, "Times New Roman", serif; font-size: 48px; line-height: 0.95; } .result-confidence { margin-top: 10px; max-width: 17rem; color: rgba(21, 32, 42, 0.66); font-size: 15px; line-height: 1.55; } .result-ring { width: 160px; height: 160px; border-radius: 50%; display: grid; place-items: center; justify-self: end; background: conic-gradient(from -90deg, #5c93c0 0 0%, rgba(35, 71, 102, 0.08) 0% 100%); } .result-ring-inner { width: 114px; height: 114px; border-radius: 50%; background: rgba(246, 250, 255, 0.96); display: grid; place-items: center; text-align: center; } .result-ring-value { font-family: Georgia, "Times New Roman", serif; font-size: 30px; } .result-ring-label { margin-top: 4px; color: rgba(21, 32, 42, 0.54); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; } .normal-result { --result-accent: var(--clear); } .harmful-result { --result-accent: var(--danger); } .error-result { --result-accent: var(--danger); } .neutral-result { --result-accent: var(--accent); } @media (max-width: 1100px) { .topbar { align-items: flex-start; flex-direction: column; } .prototype-stage { grid-template-columns: 1fr; } .queue { max-width: 100%; } } @media (max-width: 680px) { .prototype-result { grid-template-columns: 1fr; } .result-ring { justify-self: start; } } """ Classifier = Callable[[str], tuple[str, float]] def detect_missing_model_artifacts(model_dir: Path | str) -> list[str]: path = Path(model_dir) missing: list[str] = [] if not (path / "config.json").exists(): missing.append("config.json") if not any((path / name).exists() for name in WEIGHT_FILENAMES): missing.append("model weights (model.safetensors or pytorch_model.bin)") if not any((path / name).exists() for name in TOKENIZER_FILENAMES): missing.append("tokenizer assets") return missing def build_colab_mount_hint() -> str: return ( "Mount Google Drive first with: " "from google.colab import drive; drive.mount('/content/drive')" ) def build_model_path_hint(model_path: Path | str) -> str: path = str(model_path) if path.startswith("/content/drive/"): return build_colab_mount_hint() return ( "Check that the local model path is correct or set " "HATEBERT_MODEL_PATH to your model directory." ) def get_bundled_model_path(base_dir: Path | str | None = None) -> Path: root = Path(base_dir) if base_dir is not None else Path(__file__).resolve().parent return root / "fine_tuned_hatebert_model" def resolve_model_path(base_dir: Path | str | None = None) -> str: env_path = os.getenv("HATEBERT_MODEL_PATH") if env_path: return env_path bundled = get_bundled_model_path(base_dir) if bundled.exists(): return str(bundled) return DEFAULT_MODEL_PATH def get_transformers_load_kwargs() -> dict[str, bool]: return {"local_files_only": True} def build_launch_kwargs() -> dict[str, object]: env_port = os.getenv("GRADIO_SERVER_PORT") or os.getenv("PORT") server_port = int(env_port) if env_port else None running_on_space = bool(os.getenv("SPACE_ID") or os.getenv("SPACE_HOST")) return { "share": not running_on_space, "debug": not running_on_space, "server_name": "0.0.0.0", "server_port": server_port, "show_error": True, } def validate_model_path(model_path: Path | str) -> tuple[bool, str]: path = Path(model_path) if not path.exists(): return False, f"Model path does not exist: {path}. {build_model_path_hint(path)}" if not path.is_dir(): return False, f"Model path is not a directory: {path}" missing = detect_missing_model_artifacts(path) if missing: joined = ", ".join(missing) return False, f"Missing required files in model directory: {joined}" return True, f"Model directory ready: {path}" def render_empty_state() -> str: return """
Prediction
Awaiting input
Enter a short online comment to run inference.
0%
Confidence
""" def render_error_state(message: str) -> str: return f"""
Prediction unavailable
Startup issue
{message}
0%
Confidence
""" def render_result_state(label: str, confidence: float) -> str: state_class = "normal-result" if label == "Normal" else "harmful-result" accent = "var(--clear)" if label == "Normal" else "var(--danger)" confidence_pct = confidence * 100 return f"""
Prediction
{label}
Model confidence: {confidence:.2%}
{confidence:.2%}
Confidence
""" def predict_text(text: str | None, classify: Classifier) -> str: cleaned = (text or "").strip() if not cleaned: return render_empty_state() try: label, confidence = classify(cleaned) except Exception as exc: return render_error_state(f"Inference unavailable: {exc}") return render_result_state(label, confidence) def build_hero_markup() -> str: return """
H
HateBERT Moderation Prototype
Platform Safety / Live Inference System
Binary classifier / Real-time review
Content safety intelligence

Moderation infrastructure for live social risk screening.

A polished live review surface for harmful content detection. This interface frames the model as a platform moderation tool rather than a plain classroom demo.

System
Moderation
Model
HateBERT
Mode
Live Review
Binary classifier
Confidence output
2 classes
82%
Flagged Comment
User-submitted social content is routed into a lightweight review layer with confidence evidence.
Queued
2
Output Classes
The interface returns a binary moderation decision designed for clear academic demonstration and platform governance storytelling.
Normal / Harmful
Designed to look and behave like a real platform safety product while staying lightweight enough for sharing and live presentation.
""" def load_runtime(model_path: str) -> Classifier: ok, message = validate_model_path(model_path) if not ok: raise RuntimeError(message) import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer load_kwargs = get_transformers_load_kwargs() tokenizer = AutoTokenizer.from_pretrained(model_path, **load_kwargs) model = AutoModelForSequenceClassification.from_pretrained(model_path, **load_kwargs) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) model.eval() def classify(cleaned_text: str) -> tuple[str, float]: inputs = tokenizer( cleaned_text, return_tensors="pt", truncation=True, padding=True, max_length=128, ) inputs = {key: value.to(device) for key, value in inputs.items()} with torch.no_grad(): logits = model(**inputs).logits probabilities = torch.softmax(logits, dim=-1)[0] predicted_id = int(torch.argmax(probabilities).item()) confidence = float(probabilities[predicted_id].item()) label = LABELS.get(predicted_id, f"LABEL_{predicted_id}") return label, confidence return classify def build_demo(classify: Classifier): import gradio as gr with gr.Blocks(title="Moderation Demo", css=LIGHT_KEYNOTE_CSS) as demo: with gr.Column(elem_classes=["prototype-shell"]): gr.HTML(build_hero_markup()) with gr.Column(elem_classes=["prototype-panel"]): gr.HTML( """
Inference Workspace
Live Comment Review
Fine-tuned model
""" ) text_input = gr.Textbox( label="Comment Input", lines=7, placeholder="Enter an online comment for classification...", ) with gr.Row(): analyze_button = gr.Button("Run Moderation", variant="primary") clear_button = gr.Button("Clear", variant="secondary") output = gr.HTML(value=render_empty_state()) gr.Examples( examples=[ ["I really enjoy spending time with my friends at the park."], ["You are absolutely useless and I hate everything about you."], ["This is a neutral statement about the weather."], ], inputs=text_input, label="Live Review Examples", ) analyze_button.click( fn=lambda text: predict_text(text, classify), inputs=text_input, outputs=output, ) clear_button.click( fn=lambda: ("", render_empty_state()), outputs=[text_input, output], ) text_input.submit( fn=lambda text: predict_text(text, classify), inputs=text_input, outputs=output, ) return demo def main() -> None: """ Local quick start: 1. Put the fine-tuned model directory on this machine. 2. Set HATEBERT_MODEL_PATH if your model folder is not the default path. 3. Run this script to launch the Gradio demo. 4. Share the temporary public link with your teammates. """ classify = load_runtime(resolve_model_path()) demo = build_demo(classify) demo.launch(**build_launch_kwargs()) if __name__ == "__main__": main()