File size: 3,515 Bytes
dee6e6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Demo Space for ZeroGPU/zlm-v1-iab-domain-classifier.

Type a URL or bare domain; the model classifies the destination into IAB Content and
Audience categories from the URL string alone (no page fetch).
"""

import importlib.util
import json

import gradio as gr
import numpy as np
import onnxruntime as ort
from huggingface_hub import hf_hub_download
from tokenizers import Tokenizer

REPO = "ZeroGPU/zlm-v1-iab-domain-classifier"
TOP_K = 6

model_path = hf_hub_download(REPO, "onnx/model_quantized.onnx")
tokenizer = Tokenizer.from_file(hf_hub_download(REPO, "tokenizer.json"))
meta = json.load(open(hf_hub_download(REPO, "multi_head_classifier_metadata.json")))
thresholds = json.load(open(hf_hub_download(REPO, "thresholds.json")))

spec = importlib.util.spec_from_file_location("url_text", hf_hub_download(REPO, "url_text.py"))
url_text = importlib.util.module_from_spec(spec)
spec.loader.exec_module(url_text)

session = ort.InferenceSession(model_path)
output_names = [o.name for o in session.get_outputs()]


def _head_output(head: str, outputs: dict) -> np.ndarray | None:
    """Match an output tensor to a head the same way the production service does."""
    if head in outputs:
        return outputs[head]
    if head + "_logits" in outputs:
        return outputs[head + "_logits"]
    needle = head.lower().replace("_names", "")
    for name, tensor in outputs.items():
        if needle in name.lower():
            return tensor
    return None


def classify(url: str):
    url = (url or "").strip()
    if not url:
        return "", {}, {}
    text = url_text.url_to_text(url)
    enc = tokenizer.encode(text)
    raw = session.run(None, {
        "input_ids": np.array([enc.ids], dtype=np.int64),
        "attention_mask": np.array([enc.attention_mask], dtype=np.int64),
    })
    outputs = dict(zip(output_names, raw))

    results = []
    for head in ("iabcontent_names", "iabaudience_names"):
        logits = _head_output(head, outputs)
        if logits is None:
            results.append({})
            continue
        probs = 1.0 / (1.0 + np.exp(-np.asarray(logits).reshape(-1)))
        cut = thresholds.get(head, {}).get("_default", 0.5)
        labels = meta["label_mappings"]["category_labels"][head]
        picked = [(labels[i], float(p)) for i, p in enumerate(probs) if p >= cut]
        picked.sort(key=lambda t: -t[1])
        results.append(dict(picked[:TOP_K]) or {"(no label above threshold %.2f)" % cut: 0.0})
    content, audience = results
    return text, content, audience


demo = gr.Interface(
    fn=classify,
    inputs=gr.Textbox(label="URL or domain", placeholder="espn.com"),
    outputs=[
        gr.Textbox(label="Model input (url_to_text rendering)"),
        gr.Label(label="IAB Content categories", num_top_classes=TOP_K),
        gr.Label(label="IAB Audience categories", num_top_classes=TOP_K),
    ],
    title="IAB Domain Classifier",
    description=(
        "URL-only IAB classification with "
        "[ZeroGPU/zlm-v1-iab-domain-classifier](https://huggingface.co/ZeroGPU/zlm-v1-iab-domain-classifier) "
        "— a fine-tuned 149M ModernBERT that beats GPT-5.4-nano on this task "
        "(content micro-F1 0.385 vs 0.353 on a 784-URL gold benchmark) at ~36 ms per URL. "
        "No page is fetched: every signal comes from the URL string itself."
    ),
    examples=[["espn.com"], ["https://www.sportsnews.co.uk/foo"], ["techcrunch.com"], ["allrecipes.com"]],
    cache_examples=False,
)

if __name__ == "__main__":
    demo.launch()