Spaces:
Running on Zero
Running on Zero
Download app.py from hugging-apps/gliner25-multi-decide-demo: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/spaces/hugging-apps/gliner25-multi-decide-demo/resolve/main/app.py
- Command line
-
hf download hf://spaces/hugging-apps/gliner25-multi-decide-demo/app.py
-
curl -L -o app.py https://huggingface.co/spaces/hugging-apps/gliner25-multi-decide-demo/resolve/main/app.py
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) | |
| 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) |