multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
639a90d verified
Raw History Blame Contribute Delete
13.9 kB
"""GLiNER2.5-multi-Decide demo — multilingual schema-driven classification on ZeroGPU.
One 287M multilingual checkpoint answers any label set you pass at call time:
intent, routing, sentiment, priority, policy, multi-label tags. No prompt
template, no generated tokens, one forward pass per call.
"""
import spaces # MUST come before torch / any CUDA-touching import
import json
import gradio as gr
from gliner2 import AutoExtractor
MODEL_ID = "fastino/GLiNER2.5-multi-Decide"
CACHE_VERSION = 2 # bump when example rows change, so cached outputs regenerate
model = AutoExtractor.from_pretrained(MODEL_ID, map_location="cuda")
model.eval()
CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
HEADERS = ["Decision", "Labels (comma-separated)", "Multi-label", "Threshold"]
DEFAULT_ROWS = [
["sentiment", "positive, negative, mixed, neutral", False, 0.5],
]
OVERRIDES_HELP = """
Optional JSON merged into a decision **by name**. Use it for a question prompt,
label descriptions, or any other per-head config:
```json
{"answer": {"prompt": "Did the treaty enter into force in 1992?"}}
```
"""
def _to_rows(value) -> list:
"""Normalise a gr.Dataframe value (array, DataFrame, or dict) into rows."""
if value is None:
return []
if hasattr(value, "values"): # pandas.DataFrame
return value.values.tolist()
if isinstance(value, dict): # {"headers": ..., "data": ...}
return value.get("data") or []
return list(value)
def _as_float(value, fallback: float) -> float:
"""Best-effort float coercion for a table cell."""
try:
if value is None or value == "":
return fallback
return float(value)
except (TypeError, ValueError):
return fallback
def _as_bool(value) -> bool:
"""Best-effort bool coercion for a table cell."""
if isinstance(value, str):
return value.strip().lower() in {"true", "1", "yes", "multi", "on"}
if not isinstance(value, bool):
return False
return value
def build_tasks(rows, overrides_json: str) -> dict:
"""Turn the decisions table (plus optional JSON overrides) into a tasks dict.
Args:
rows: Table rows of ``[decision, labels, multi_label, threshold]``.
overrides_json: JSON object merged into each named decision.
Returns:
``{"tasks": {...}, "errors": [...]}`` where ``tasks`` is the mapping
accepted by ``classify_text``.
"""
tasks: dict = {}
errors: list = []
for row in _to_rows(rows):
cells = list(row) + [None] * (4 - len(row))
name = str(cells[0] or "").strip()
raw_labels = cells[1]
if isinstance(raw_labels, (list, tuple)):
labels = [str(l).strip() for l in raw_labels if str(l).strip()]
else:
labels = [l.strip() for l in str(raw_labels or "").split(",") if l.strip()]
if not name and not labels:
continue
if not name:
errors.append("Every row needs a decision name.")
continue
if len(labels) < 2:
errors.append(f"“{name}” needs at least 2 labels.")
continue
if name in tasks:
errors.append(f"“{name}” appears twice — decision names must be unique.")
continue
tasks[name] = {
"labels": labels,
"multi_label": _as_bool(cells[2]),
"cls_threshold": _as_float(cells[3], 0.5),
}
raw_overrides = (overrides_json or "").strip()
if raw_overrides and raw_overrides != "{}":
try:
extra = json.loads(raw_overrides)
if not isinstance(extra, dict):
raise ValueError("must be a JSON object")
except (json.JSONDecodeError, ValueError) as exc:
errors.append(f"Overrides JSON is invalid: {exc}")
else:
for name, cfg in extra.items():
if not isinstance(cfg, dict):
errors.append(f"Override for “{name}” must be an object.")
continue
task = tasks.setdefault(name, {})
task.update(cfg)
if not task.get("labels"):
errors.append(f"“{name}” has no usable labels.")
return {"tasks": tasks, "errors": errors}
def summarize(result: dict, include_confidence: bool) -> str:
"""Render the classification payload as one line per decision head."""
if not result:
return "_No decision was returned._"
lines = []
for name, value in result.items():
if isinstance(value, list):
parts = []
for item in value:
label = item.get("label") if isinstance(item, dict) else item
conf = item.get("confidence") if isinstance(item, dict) else None
if include_confidence and conf is not None:
parts.append(f"`{label}` ({conf:.2f})")
else:
parts.append(f"`{label}`")
rendered = ", ".join(parts) if parts else "_(none)_"
elif isinstance(value, dict):
rendered = f"`{value.get('label')}`"
if include_confidence and value.get("confidence") is not None:
rendered = f"{rendered} ({value['confidence']:.2f})"
else:
rendered = f"`{value}`"
lines.append(f"- **{name}** → {rendered}")
return "\n".join(lines)
@spaces.GPU(duration=30)
def decide(
text: str,
rows: list,
overrides_json: str = "{}",
include_confidence: bool = False,
) -> tuple:
"""Classify text against your own label set with GLiNER2.5-multi-Decide.
Works across languages: the model is a 287M multilingual encoder
(mDeBERTa-v3-base) that scores every decision head in one forward pass.
Args:
text: The text to decide about, in any language.
rows: Decisions table — one row per decision head: decision name, its
comma-separated labels, whether it is multi-label, and the
confidence threshold. All rows are scored in a single call.
overrides_json: Optional JSON object merged into a decision by name, for
a question prompt, label descriptions, or extra config.
include_confidence: When true, keep the confidence score of each label.
Returns:
The raw classification payload as JSON, and a short readable summary.
"""
if not isinstance(include_confidence, bool):
include_confidence = False
if not (text or "").strip():
return "{}", "_Enter some text to classify._"
built = build_tasks(rows, overrides_json)
tasks, errors = built["tasks"], built["errors"]
if not tasks:
detail = " ".join(errors) or "Add at least one decision with two or more labels."
return "{}", f"_{detail}_"
result = model.classify_text(text, tasks, include_confidence=include_confidence)
payload = json.dumps(result, indent=2, ensure_ascii=False, default=str)
summary = summarize(result, include_confidence)
if errors:
summary = "⚠️ " + " ".join(errors) + "\n\n" + summary
return payload, summary
with gr.Blocks(title="GLiNER2.5-multi-Decide") as demo:
gr.Markdown(
"""
# 🌍 GLiNER2.5-multi-Decide
One 287M **multilingual** checkpoint, **any label set you pass at call time** — intent,
routing, sentiment, priority, moderation, multi-label tags — in one forward pass.
No prompt template, no generated tokens. Type the text in any language, then edit the
**decisions table**: each row is one decision head, and every row is scored together in
the same call. A single-label head returns one label; a multi-label head returns every
label above its threshold.
"""
)
with gr.Row():
with gr.Column(scale=3):
text_in = gr.Textbox(
label="Text",
placeholder="Paste the message, ticket, review or document to decide about — any language…",
lines=6,
)
decisions = gr.Dataframe(
headers=HEADERS,
datatype=["str", "str", "bool", "number"],
type="array",
column_count=4,
row_count=4,
value=DEFAULT_ROWS,
label="Decisions",
wrap=True,
)
with gr.Accordion("Advanced options", open=False):
gr.Markdown(OVERRIDES_HELP)
overrides = gr.Code(
label="Per-decision overrides (JSON, optional)",
language="json",
value="{}",
lines=4,
)
include_conf = gr.Checkbox(
label="Include confidence scores",
value=False,
)
run = gr.Button("Decide", variant="primary")
with gr.Column(scale=2):
summary_out = gr.Markdown(label="Decisions")
json_out = gr.Code(label="Raw output (JSON)", language="json", lines=14)
run.click(
decide,
inputs=[text_in, decisions, overrides, include_conf],
outputs=[json_out, summary_out],
api_name="decide",
)
gr.Examples(
examples=[
[
"My subscription renewed on April 15 for ¥5,400 after the service was already down. Can I get that charge refunded?",
[["intent", "order_status, refund_request, cancel_subscription, update_payment, login_problem, shipping_delay, bug_report, speak_to_human, other", False, 0.5]],
"{}",
],
[
"Bonjour, mon abonnement a été renouvelé le 15 avril pour 5 400 ¥ alors que le service était en panne. Puis-je être remboursé de ce prélèvement ?",
[["intention", "statut_commande, demande_remboursement, annulation_abonnement, maj_paiement, probleme_connexion, retard_livraison, rapport_bug, parler_humain, autre", False, 0.5]],
"{}",
],
[
"El turno de mañana lo cubre Marta. Necesito cambiar mi vuelo del viernes a París al sábado por la mañana, la misma cabina.",
[["peticion", "reservar, cambiar, cancelar, estado, cambio_asiento, reembolso, equipaje", False, 0.5]],
"{}",
],
[
"Battery dies before lunch, but the keyboard and the screen are the best I have used on a laptop.",
[
["sentiment", "positive, negative, mixed, neutral", False, 0.5],
["aspects", "battery, keyboard, screen, camera, price, support", True, 0.4],
],
"{}",
],
[
"バッテリーは昼前になくなりますが、キーボードと画面はこれまで使ったノートパソコンの中で最高です。",
[["sentiment", "positive, negative, mixed, neutral", False, 0.5]],
"{}",
],
[
"From: compliance@group.example\nSubject: Protocol update — action required today\n\nPlease confirm the new retention rule is applied before Friday's audit.",
[
["intent", "fyi, request, approval, complaint, newsletter, security_alert", False, 0.5],
["urgency", "low, normal, high, critical", False, 0.5],
["route", "support, billing, legal, security, finance, archive", False, 0.5],
],
"{}",
],
[
"Guest in room 1408 says the AC has been out since yesterday and they want to move tonight or leave. They also asked for the incidentals hold to be released.",
[
["intent", "maintenance, room_change, checkout, billing, complaint, amenity_request", False, 0.5],
["priority", "low, normal, high, urgent", False, 0.5],
["needs_human", "yes, no", False, 0.5],
["topics", "hvac, billing, housekeeping, noise, safety", True, 0.4],
],
"{}",
],
[
"This is the third time I have explained the same missing refund. Stop the bot and get me a person.",
[["handoff", "yes, no", False, 0.5]],
"{}",
],
[
"Payroll file has to be corrected before the 5pm cutoff or the whole company is paid late.",
[["urgency", "0, 1, 2, 3, 4, 5", False, 0.5]],
"{}",
],
[
"The treaty was signed in Paris in 1992. It entered into force the following year, after the last signatory ratified it.",
[["answer", "yes, no", False, 0.5]],
'{"answer": {"prompt": "Did the treaty enter into force in 1992?"}}',
],
[
"Please reset the card PIN. The new one never arrived and the old one is locked after three tries.",
[["intent", "card_pin_change, card_lost, balance_inquiry", False, 0.5]],
'{"intent": {"labels": {"card_pin_change": "The customer wants a new PIN or the current PIN replaced", "card_lost": "The physical card is missing", "balance_inquiry": "The customer wants the current balance"}}}',
],
],
inputs=[text_in, decisions, overrides],
outputs=[json_out, summary_out],
fn=decide,
cache_examples=True,
cache_mode="lazy",
label="Examples — multilingual decisions from the model card",
)
demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS)