File size: 7,782 Bytes
623acb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf25444
 
 
623acb0
bf25444
 
623acb0
bf25444
 
 
 
 
 
623acb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf25444
623acb0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf25444
623acb0
bf25444
623acb0
 
 
 
 
b5d1d0f
 
 
623acb0
 
 
 
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
152
153
154
155
156
157
"""Grounding demo — is a claim supported by the document it cites?

Runs BOTH grounding models side by side:
  * grounding-en              (open weights, English)
  * grounding-multilingual    (commercial; loaded from a PRIVATE repo via the HF_TOKEN Space secret,
                               so its outputs are shown but the weights are never downloadable here)

Each score is a calibrated support probability: p = softmax(logits / T)[entailment_index]. Temperature T
is per-model (monotonic — it fixes the confidence value, not the ranking).
"""
import os

import gradio as gr
import spaces
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer

EI = 0  # entailment class index for the *-zeroshot-v2.0 heads (id2label {0: entailment, 1: not_entailment})

MODELS = [
    {"key": "en", "name": "grounding-en", "tag": "open · English",
     "repo": os.environ.get("EN_REPO", "nutrientdocs/grounding-en"), "T": 1.2866, "private": False},
    {"key": "multi", "name": "grounding-multilingual", "tag": "commercial · 15+ languages",
     "repo": os.environ.get("ML_REPO", "nutrientdocs/grounding-multilingual-private"),
     "subfolder": "weights", "T": 0.9432, "private": True},
]

TOKEN = os.environ.get("HF_TOKEN")  # Space secret; required for the private commercial model
_loaded = {}

# Model selector -> which model keys to score. A model card deep-links here with ?model=en|multi so
# clicking "Try it" from a model page opens the demo focused on that model.
SELECTION = {
    "grounding-en (English)": ["en"],
    "grounding-multilingual": ["multi"],
    "Both (compare)": ["en", "multi"],
}


def _from_query(request: gr.Request):
    m = (request.query_params.get("model") or "").lower()
    if m == "en":
        return "grounding-en (English)"
    if m in ("multi", "multilingual", "bge"):
        return "grounding-multilingual"
    return "Both (compare)"


def _ensure(spec):
    """Load tokenizer+model onto CPU and cache. Runs OUTSIDE the GPU window (no GPU needed to download),
    so a ZeroGPU allocation is spent only on the forward pass. Stores the Exception if a private model
    can't be reached (e.g. missing HF_TOKEN secret)."""
    if spec["key"] in _loaded:
        return _loaded[spec["key"]]
    kw = {"token": TOKEN} if TOKEN else {}  # both models are private; token is org-scoped
    if spec.get("subfolder"):
        kw["subfolder"] = spec["subfolder"]
    try:
        tok = AutoTokenizer.from_pretrained(spec["repo"], **kw)
        model = AutoModelForSequenceClassification.from_pretrained(spec["repo"], **kw).eval()
        _loaded[spec["key"]] = (tok, model)
    except Exception as e:
        _loaded[spec["key"]] = e
    return _loaded[spec["key"]]


@spaces.GPU(duration=120)
def _forward(premise, hypothesis, keys):
    """ZeroGPU-allocated: move each selected model to CUDA and score. Returns key -> calibrated support
    probability (None if the model failed to load)."""
    scores = {}
    for spec in MODELS:
        if spec["key"] not in keys:
            continue
        got = _loaded.get(spec["key"])
        if not isinstance(got, tuple):
            scores[spec["key"]] = None
            continue
        tok, model = got
        model = model.to("cuda")
        enc = tok(premise, hypothesis, truncation=True, max_length=1024, return_tensors="pt").to("cuda")
        with torch.no_grad():
            logits = model(**enc).logits
        scores[spec["key"]] = torch.softmax(logits / spec["T"], dim=-1)[0, EI].item()  # calibrated
    return scores


def grade(premise, hypothesis, selection):
    # Generator: emit an immediate status line so the click has visible feedback while the (potentially
    # slow) cold start runs — first request downloads the weights to CPU AND cold-starts a shared ZeroGPU
    # allocation, which together can take ~30-60s. Warm requests only wait on the GPU handoff.
    if not premise.strip() or not hypothesis.strip():
        yield "Enter a document premise and a claim to check."
        return
    keys = SELECTION.get(selection, ["en", "multi"])
    cold = [s for s in MODELS if s["key"] in keys and s["key"] not in _loaded]
    if cold:
        yield ("⏳ **Waking up…** the first check downloads the model and cold-starts a shared GPU — "
               "this can take **~30–60s**. Later checks are near-instant.")
    else:
        yield "⏳ Scoring on the GPU…"
    for spec in MODELS:            # download/load on CPU first, so the GPU window is inference-only
        if spec["key"] in keys:
            _ensure(spec)
    scores = _forward(premise, hypothesis, keys)
    lines = ["| Model | | Grounded support | Verdict |", "|---|---|---:|---|"]
    for spec in MODELS:
        if spec["key"] not in keys:
            continue
        p = scores.get(spec["key"])
        if p is None:
            lines.append(f"| **{spec['name']}** | {spec['tag']} | _unavailable_ | "
                         "_(commercial — set HF_TOKEN)_ |")
            continue
        verdict = "✅ grounded" if p >= 0.5 else "❌ not grounded"
        bar = "█" * round(p * 10) + "░" * (10 - round(p * 10))
        lines.append(f"| **{spec['name']}** | {spec['tag']} | `{bar}` **{p:.3f}** | {verdict} |")
    yield "\n".join(lines)


from examples import EXAMPLES  # noqa: E402  (real, labeled rows; single source of truth, verified)

with gr.Blocks(title="Grounding demo", theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        "# Does the document actually support this claim?\n"
        "Paste a **document premise** (a table or passage) and a **claim**. The grounding model scores "
        "whether the document *supports* the claim. Scores are temperature-calibrated support "
        "probabilities; the verdict is the model's decision at 0.5.\n\n"
        "Pick an example below (its **expected answer** is shown) or type your own. The non-English "
        "examples are cases the English model gets wrong — switch to the **multilingual** model.\n\n"
        "→ [grounding-en (open)](https://huggingface.co/nutrientdocs/grounding-en) · "
        "[grounding-multilingual (commercial)](https://huggingface.co/nutrientdocs/grounding-multilingual) · "
        "[leaderboard](https://huggingface.co/spaces/nutrientdocs/grounding-leaderboard) · "
        "[benchmark](https://huggingface.co/datasets/nutrientdocs/grounding-benchmark)")
    model_sel = gr.Radio(list(SELECTION), value="Both (compare)", label="Model")
    with gr.Row():
        premise = gr.Textbox(label="Document premise", lines=8)
        hypothesis = gr.Textbox(label="Claim to check", lines=8)
    expected = gr.Textbox(label="Expected answer (ground truth for the selected example)",
                          interactive=False)
    btn = gr.Button("Check grounding", variant="primary")
    gr.Markdown("<sub>⏱️ The first check cold-starts a shared GPU (~30–60s); later checks are fast.</sub>")
    out = gr.Markdown()
    btn.click(grade, [premise, hypothesis, model_sel], out, show_progress="full")
    gr.Examples(EXAMPLES, [premise, hypothesis, expected])  # fills the boxes incl. expected answer
    gr.Markdown(
        '## About the author\n'
        '<a href="https://nutrient.io/">'
        '<img src="https://avatars2.githubusercontent.com/u/1527679?v=3&s=200" height="80" /></a>\n\n'
        "This demo is maintained and funded by [Nutrient](https://nutrient.io/) — "
        "The deterministic document infrastructure enterprises run their highest-stakes workflows on: "
        "replayable output, clear exceptions, and full audit trails on the messy, regulated documents where AI alone breaks.")
    demo.load(_from_query, None, model_sel)                 # ?model=en|multi presets the selector

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