Instructions to use davanstrien/hub-task-tagger-gliner2.5-base with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- GLiNER2
How to use davanstrien/hub-task-tagger-gliner2.5-base with GLiNER2:
from gliner2 import AutoExtractor extractor = AutoExtractor.from_pretrained("davanstrien/hub-task-tagger-gliner2.5-base") # Extract entities text = "Apple CEO Tim Cook announced iPhone 15 in Cupertino yesterday." result = extractor.extract_entities(text, ["company", "person", "product", "location"]) print(result) - Notebooks
- Google Colab
- Kaggle
Model card (draft for review)
#1
by davanstrien HF Staff - opened
- README.md +131 -0
- tagger_core.py +200 -0
README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: fastino/gliner2.5-base-v1
|
| 4 |
+
library_name: gliner2
|
| 5 |
+
pipeline_tag: text-classification
|
| 6 |
+
language:
|
| 7 |
+
- en
|
| 8 |
+
tags:
|
| 9 |
+
- gliner2
|
| 10 |
+
- text-classification
|
| 11 |
+
- multi-label
|
| 12 |
+
- hub-metadata
|
| 13 |
+
- hf-jobs
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
# Hub task tagger (GLiNER2.5-base)
|
| 17 |
+
|
| 18 |
+
Suggests task tags for a Hugging Face dataset, such as `text-generation`, `robotics` or
|
| 19 |
+
`automatic-speech-recognition`, from its column names and first row. It picks from the 52 task
|
| 20 |
+
tags the Hub offers, and gives a probability for each.
|
| 21 |
+
|
| 22 |
+
[TypeSafe AI's Jev](https://docs.typesafe.ai/introduction) has got people interested in
|
| 23 |
+
"System One" models: models that don't generate text, but read a state and return typed answers
|
| 24 |
+
with probabilities. This is a small, open example of the same idea for one narrow job, fine-tuned
|
| 25 |
+
in 17 minutes on [Hugging Face Jobs](https://huggingface.co/docs/hub/jobs).
|
| 26 |
+
|
| 27 |
+
**Try it:** [davanstrien/hub-task-tagger](https://huggingface.co/spaces/davanstrien/hub-task-tagger).
|
| 28 |
+
Paste a dataset id and see the suggested tags, the owner's tags, and the exact text the model read.
|
| 29 |
+
|
| 30 |
+
## How to use
|
| 31 |
+
|
| 32 |
+
The model reads a short text built from the dataset viewer's preview: a `Columns:` line with each
|
| 33 |
+
column's name and type, then the first row, cut to 370 tokens. It must be built exactly as in
|
| 34 |
+
training, so this repo ships the builder,
|
| 35 |
+
[`tagger_core.py`](https://huggingface.co/davanstrien/hub-task-tagger-gliner2.5-base/blob/main/tagger_core.py),
|
| 36 |
+
next to the weights. The snippet below downloads it with `huggingface_hub` and uses it.
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
import json, math, os, sys
|
| 40 |
+
from huggingface_hub import hf_hub_download
|
| 41 |
+
from tokenizers import Tokenizer
|
| 42 |
+
from gliner2.classification import Classifier, ClassificationSchema, ClassificationConfig
|
| 43 |
+
|
| 44 |
+
repo = "davanstrien/hub-task-tagger-gliner2.5-base"
|
| 45 |
+
|
| 46 |
+
# 1. Build the model's input for a dataset, with the code that built the training data.
|
| 47 |
+
path = hf_hub_download(repo, "tagger_core.py")
|
| 48 |
+
sys.path.insert(0, os.path.dirname(path)) # make the downloaded file importable
|
| 49 |
+
import tagger_core as core
|
| 50 |
+
|
| 51 |
+
builder = core.StateBuilder(Tokenizer.from_file(hf_hub_download("Qwen/Qwen3.5-4B-Base", "tokenizer.json")), 370)
|
| 52 |
+
|
| 53 |
+
first_rows, config, split, error = core.fetch_first_rows("fancyzhx/ag_news")
|
| 54 |
+
state, truncated = builder.build(core.render_first_rows(first_rows))
|
| 55 |
+
print(state)
|
| 56 |
+
|
| 57 |
+
# 2. Score all 52 tags.
|
| 58 |
+
labels = json.load(open(hf_hub_download(repo, "classification_schema.json")))["tasks"][0]["labels"]
|
| 59 |
+
model = Classifier.from_pretrained(repo).eval()
|
| 60 |
+
schema = ClassificationSchema().multi("labels", labels)
|
| 61 |
+
logits = model.batch_score([state[:2000]], schema, config=ClassificationConfig(batch_size=1))[0].tasks["labels"]
|
| 62 |
+
|
| 63 |
+
# 3. Calibrated probabilities: softmax over the 52 logits at temperature 1.30 (fitted on held-out data).
|
| 64 |
+
z = [logits[label] / 1.30 for label in labels]
|
| 65 |
+
m = max(z)
|
| 66 |
+
e = [math.exp(v - m) for v in z]
|
| 67 |
+
probs = sorted(zip(labels, (v / sum(e) for v in e)), key=lambda x: -x[1])
|
| 68 |
+
print(probs[:3])
|
| 69 |
+
|
| 70 |
+
# A set of tags: everything at 0.225 or above, and always the top tag.
|
| 71 |
+
suggested = [label for label, p in probs if p >= 0.225] or [probs[0][0]]
|
| 72 |
+
print(suggested)
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
The Qwen tokenizer is only used to measure and cut the input to the length the model was trained
|
| 76 |
+
on. `fetch_first_rows` returns an error instead of rows when a dataset has no viewer preview.
|
| 77 |
+
|
| 78 |
+
Install with `pip install "gliner2[local]==2.0.0"`. Weights are fp32. On a free 2-vCPU Space one
|
| 79 |
+
prediction takes about 0.7–1 s; on an L4 GPU in fp16, about 20 ms.
|
| 80 |
+
|
| 81 |
+
## Training
|
| 82 |
+
|
| 83 |
+
- **Recipe:** [`train-gliner2.py`](https://huggingface.co/datasets/uv-scripts/classification) from
|
| 84 |
+
`uv-scripts/classification`, run as one `hf jobs uv run` command on an `rtx-pro-6000`:
|
| 85 |
+
`--base-model fastino/gliner2.5-base-v1 --labels-file labels.json --label-column labels --label-augmentation off`,
|
| 86 |
+
5 epochs, seed 0, bf16. About 17 minutes of training, roughly $1.50.
|
| 87 |
+
- **Data:** 16,000 public Hub datasets with owner-declared `task_categories`. Each example is the
|
| 88 |
+
dataset's column names and first row (from the dataset viewer), cut to about 370 tokens, and its
|
| 89 |
+
declared tags (multi-label). Retired tags were mapped to current ones where the task is the same
|
| 90 |
+
(for example `text2text-generation` → `text-generation`); others were dropped.
|
| 91 |
+
- **Split by time and owner:** the evaluation sets are 1,000 (calibration) and 3,000 (development)
|
| 92 |
+
datasets created in the 60 days before 2026-09-22, by owners who have no dataset in the training
|
| 93 |
+
data. Near-duplicate datasets from the same owner cannot leak between training and evaluation.
|
| 94 |
+
|
| 95 |
+
## Evaluation
|
| 96 |
+
|
| 97 |
+
On the 3,000 development datasets. A prediction counts as correct if it is one of the owner's tags.
|
| 98 |
+
|
| 99 |
+
| | Top-1 | Top-3 | Macro recall (24 tags) |
|
| 100 |
+
|---|---|---|---|
|
| 101 |
+
| This model | **0.695** | 0.881 | 0.525 |
|
| 102 |
+
| Same recipe, second seed | 0.686 | 0.879 | 0.511 |
|
| 103 |
+
| GLiNER2.5-small, same recipe | 0.653 | 0.855 | 0.458 |
|
| 104 |
+
| GLiNER2.5-base, zero-shot | 0.102 | 0.213 | 0.099 |
|
| 105 |
+
| Always "text-generation" | 0.320 | – | – |
|
| 106 |
+
|
| 107 |
+
- **The owners' tags are a noisy target.** In a hand-checked sample, about 1 dataset in 10 was
|
| 108 |
+
missing a tag that fits. Counting 74 card-checked extra tags as correct raises top-1 to 0.720.
|
| 109 |
+
- **As a set** (all tags at probability ≥ 0.225), micro-F1 against the owner's tags is 0.623,
|
| 110 |
+
with 1.21 tags per dataset on average against 1.32 declared.
|
| 111 |
+
- **Confidence:** the temperature and the threshold were fitted on the calibration set. Thresholds
|
| 112 |
+
chosen there held on development within a few points of error, but not exactly: treat the
|
| 113 |
+
probabilities as a good ranking, not as exact rates.
|
| 114 |
+
|
| 115 |
+
## Limitations
|
| 116 |
+
|
| 117 |
+
- **It only sees the first row.** Datasets whose purpose is not visible from the columns and one
|
| 118 |
+
example (for example, generic tables) are hard.
|
| 119 |
+
- **Tabular tags are weak.** It often confuses `tabular-regression` with `tabular-classification`,
|
| 120 |
+
because the input does not say which column is the target.
|
| 121 |
+
- **Rare tags** (17 of the 52, such as `text-to-3d` or `voice-activity-detection`) had too few
|
| 122 |
+
training examples to measure.
|
| 123 |
+
- **It learned from owners' choices,** including their habits: for example, question-answering
|
| 124 |
+
datasets tagged `text-generation`, or the reverse.
|
| 125 |
+
- Trained and evaluated mostly on English-language metadata.
|
| 126 |
+
|
| 127 |
+
## Credits
|
| 128 |
+
|
| 129 |
+
Built on [GLiNER2](https://github.com/fastino-ai/GLiNER2) by Fastino. Training, evaluation and the
|
| 130 |
+
demo were run on Hugging Face Jobs and Spaces, with much of the work done by coding agents (Claude)
|
| 131 |
+
and checked by a person. More detail: [GLINER2-NOTES.md](https://huggingface.co/datasets/uv-scripts/classification/blob/main/GLINER2-NOTES.md).
|
tagger_core.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendering and state building, copied verbatim from the Gradio test Space (demo-space/app.py).
|
| 2 |
+
|
| 3 |
+
That code is itself copied from build_hub_task_rows.py and prepare_hub_tasks.py, and it matched the stored
|
| 4 |
+
training states 200/200. Do not edit the functions below; change the training scripts and re-copy instead.
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
import requests
|
| 10 |
+
|
| 11 |
+
# ---- rendering: copied from build_hub_task_rows.py ------------------------------------------------------------
|
| 12 |
+
|
| 13 |
+
SERVER = "https://datasets-server.huggingface.co"
|
| 14 |
+
TAGS_URL = "https://huggingface.co/api/datasets-tags-by-type?type=task_categories"
|
| 15 |
+
RETIRED = {
|
| 16 |
+
"text2text-generation": "text-generation",
|
| 17 |
+
"conversational": "text-generation",
|
| 18 |
+
"language-modeling": "text-generation",
|
| 19 |
+
"sequence-modeling": "text-generation",
|
| 20 |
+
"document-question-answering": "visual-question-answering",
|
| 21 |
+
"multiple-choice-qa": "multiple-choice",
|
| 22 |
+
"topic-classification": "text-classification",
|
| 23 |
+
}
|
| 24 |
+
MEDIA = {"Image": "<image>", "Audio": "<audio>", "Video": "<video>", "Pdf": "<pdf>"}
|
| 25 |
+
NUMERIC = ("int", "uint", "float", "double", "bool")
|
| 26 |
+
CELL_CHARS = 200
|
| 27 |
+
ROWS = 3
|
| 28 |
+
MAX_PREVIEW_TRIES = 3
|
| 29 |
+
|
| 30 |
+
# Anonymous on purpose: the Space shows only what any visitor could read (no gated or private previews).
|
| 31 |
+
session = requests.Session()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_json(url, params):
|
| 35 |
+
"""(status, json). Retries 429/502/503/504 and network errors, honouring Retry-After; returns the last status."""
|
| 36 |
+
status = None
|
| 37 |
+
for attempt in range(5):
|
| 38 |
+
try:
|
| 39 |
+
r = session.get(url, params=params, timeout=60)
|
| 40 |
+
except requests.RequestException:
|
| 41 |
+
status = "network"
|
| 42 |
+
time.sleep(2 ** attempt)
|
| 43 |
+
continue
|
| 44 |
+
status = r.status_code
|
| 45 |
+
if status in (429, 502, 503, 504):
|
| 46 |
+
wait = r.headers.get("Retry-After", "")
|
| 47 |
+
time.sleep(min(60.0, float(wait)) if wait.replace(".", "", 1).isdigit() else 2 ** attempt)
|
| 48 |
+
continue
|
| 49 |
+
try:
|
| 50 |
+
return status, r.json()
|
| 51 |
+
except ValueError:
|
| 52 |
+
return status, {}
|
| 53 |
+
return status, {}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def type_name(t):
|
| 57 |
+
"""Short type for the Columns line: string, int64, Image, ClassLabel: a, b, list<float32>, struct."""
|
| 58 |
+
if isinstance(t, list):
|
| 59 |
+
return f"list<{type_name(t[0])}>" if t else "list"
|
| 60 |
+
if not isinstance(t, dict):
|
| 61 |
+
return "?"
|
| 62 |
+
kind = t.get("_type")
|
| 63 |
+
if kind == "Value":
|
| 64 |
+
return t.get("dtype", "?")
|
| 65 |
+
if kind == "ClassLabel":
|
| 66 |
+
names = [str(n) for n in (t.get("names") or [])]
|
| 67 |
+
if not names:
|
| 68 |
+
return "ClassLabel"
|
| 69 |
+
return "ClassLabel: " + ", ".join(names[:8]) + (", …" if len(names) > 8 else "")
|
| 70 |
+
if kind in ("List", "Sequence", "LargeList"):
|
| 71 |
+
return f"list<{type_name(t.get('feature'))}>"
|
| 72 |
+
if kind:
|
| 73 |
+
return kind
|
| 74 |
+
return "struct"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def is_numeric(t):
|
| 78 |
+
return isinstance(t, dict) and t.get("_type") == "Value" and str(t.get("dtype", "")).startswith(NUMERIC)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def render_value(v, t):
|
| 82 |
+
"""A cell value as display text, following the feature type recursively."""
|
| 83 |
+
if v is None:
|
| 84 |
+
return "null"
|
| 85 |
+
if isinstance(t, list):
|
| 86 |
+
t = {"_type": "List", "feature": t[0] if t else None}
|
| 87 |
+
kind = t.get("_type") if isinstance(t, dict) else None
|
| 88 |
+
if kind in MEDIA:
|
| 89 |
+
return MEDIA[kind]
|
| 90 |
+
if kind == "ClassLabel":
|
| 91 |
+
names = t.get("names") or []
|
| 92 |
+
return str(names[v]) if isinstance(v, int) and 0 <= v < len(names) else str(v)
|
| 93 |
+
if kind in ("List", "Sequence", "LargeList"):
|
| 94 |
+
inner = t.get("feature")
|
| 95 |
+
if isinstance(v, dict) and isinstance(inner, dict) and not inner.get("_type"):
|
| 96 |
+
return "{" + ", ".join(f"{k}: {render_value(v.get(k), {'_type': 'List', 'feature': sub})}" for k, sub in inner.items()) + "}"
|
| 97 |
+
if not isinstance(v, list):
|
| 98 |
+
return json.dumps(v, ensure_ascii=False)
|
| 99 |
+
if is_numeric(inner) and len(v) > 4:
|
| 100 |
+
return f"[{len(v)} × {inner.get('dtype')}]"
|
| 101 |
+
shown = [render_value(x, inner) for x in v[:5]]
|
| 102 |
+
return "[" + ", ".join(shown) + (f", … ({len(v)} items)" if len(v) > 5 else "") + "]"
|
| 103 |
+
if kind is None and isinstance(t, dict) and isinstance(v, dict):
|
| 104 |
+
return "{" + ", ".join(f"{k}: {render_value(v.get(k), sub)}" for k, sub in t.items()) + "}"
|
| 105 |
+
if isinstance(v, str):
|
| 106 |
+
return v
|
| 107 |
+
return json.dumps(v, ensure_ascii=False)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def cell_text(v, t, truncated):
|
| 111 |
+
s = " ".join(render_value(v, t).split())
|
| 112 |
+
if len(s) > CELL_CHARS:
|
| 113 |
+
return s[:CELL_CHARS] + "…"
|
| 114 |
+
return s + ("…" if truncated else "")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def fetch_first_rows(dataset_id):
|
| 118 |
+
"""(first_rows_json, config, split, error). The fetch half of build_hub_task_rows.preview()."""
|
| 119 |
+
status, sp = get_json(f"{SERVER}/splits", {"dataset": dataset_id})
|
| 120 |
+
splits = sp.get("splits") or []
|
| 121 |
+
if not splits:
|
| 122 |
+
return None, None, None, f"splits:{status} {sp.get('error', '')}".strip()
|
| 123 |
+
last = None
|
| 124 |
+
for s in splits[:MAX_PREVIEW_TRIES]:
|
| 125 |
+
cfg, split = s["config"], s["split"]
|
| 126 |
+
status, fr = get_json(f"{SERVER}/first-rows", {"dataset": dataset_id, "config": cfg, "split": split})
|
| 127 |
+
if not (fr.get("features") or []) or not (fr.get("rows") or []):
|
| 128 |
+
last = f"first-rows:{status} {fr.get('error', '')}".strip()
|
| 129 |
+
continue
|
| 130 |
+
return fr, cfg, split, None
|
| 131 |
+
return None, None, None, last
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def render_first_rows(fr):
|
| 135 |
+
"""The render half of build_hub_task_rows.preview()."""
|
| 136 |
+
feats, rows = fr["features"], fr["rows"]
|
| 137 |
+
types = {f["name"]: f["type"] for f in feats}
|
| 138 |
+
lines = ["Columns: " + ", ".join(f"{f['name']} ({type_name(f['type'])})" for f in feats), "Example rows:"]
|
| 139 |
+
for i, r in enumerate(rows[:ROWS], 1):
|
| 140 |
+
cut = set(r.get("truncated_cells") or [])
|
| 141 |
+
lines.append(f"{i}. " + " | ".join(f"{k}: {cell_text(v, types.get(k), k in cut)}" for k, v in r["row"].items()))
|
| 142 |
+
return "\n".join(lines)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# ---- state: copied from prepare_hub_tasks.py -----------------------------------------------------------------
|
| 146 |
+
|
| 147 |
+
COLUMNS_SHARE = 0.45
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class StateBuilder:
|
| 151 |
+
def __init__(self, tokenizer, max_tokens):
|
| 152 |
+
self.tok, self.max = tokenizer, max_tokens
|
| 153 |
+
|
| 154 |
+
def n(self, s):
|
| 155 |
+
return len(self.tok.encode(s, add_special_tokens=False).ids)
|
| 156 |
+
|
| 157 |
+
def cut(self, s, k):
|
| 158 |
+
"""First k tokens of s, with a trailing … when something was removed."""
|
| 159 |
+
ids = self.tok.encode(s, add_special_tokens=False).ids
|
| 160 |
+
if len(ids) <= k:
|
| 161 |
+
return s, False
|
| 162 |
+
return self.tok.decode(ids[: max(0, k - 1)]).rstrip() + "…", True
|
| 163 |
+
|
| 164 |
+
def build(self, text):
|
| 165 |
+
"""(state, truncated): Columns line + first example row, each cell getting a fair share of the budget."""
|
| 166 |
+
lines = text.split("\n")
|
| 167 |
+
columns = lines[0]
|
| 168 |
+
first = next((line for line in lines[1:] if line.startswith("1. ")), "")
|
| 169 |
+
truncated = False
|
| 170 |
+
if self.n(columns) > int(self.max * COLUMNS_SHARE):
|
| 171 |
+
columns, truncated = self.cut(columns, int(self.max * COLUMNS_SHARE))
|
| 172 |
+
if first:
|
| 173 |
+
cells = first[3:].split(" | ")
|
| 174 |
+
keys = [c.split(": ", 1)[0] + ": " if ": " in c else "" for c in cells]
|
| 175 |
+
values = [c.split(": ", 1)[1] if ": " in c else c for c in cells]
|
| 176 |
+
overhead = self.n(f"{columns}\nExample row:\n1. ") + sum(self.n(k) for k in keys) + 2 * len(cells)
|
| 177 |
+
budget = self.max - overhead
|
| 178 |
+
sizes = [self.n(v) for v in values]
|
| 179 |
+
if budget < len(cells):
|
| 180 |
+
cells = [k.rstrip(": ") for k in keys]
|
| 181 |
+
truncated = True
|
| 182 |
+
elif sum(sizes) > budget:
|
| 183 |
+
lo, hi = 1, max(sizes)
|
| 184 |
+
while lo < hi:
|
| 185 |
+
mid = (lo + hi + 1) // 2
|
| 186 |
+
if sum(min(s, mid) for s in sizes) <= budget:
|
| 187 |
+
lo = mid
|
| 188 |
+
else:
|
| 189 |
+
hi = mid - 1
|
| 190 |
+
cells = [k + self.cut(v, lo)[0] for k, v in zip(keys, values)]
|
| 191 |
+
truncated = True
|
| 192 |
+
state = f"{columns}\nExample row:\n1. " + " | ".join(cells)
|
| 193 |
+
else:
|
| 194 |
+
state = columns
|
| 195 |
+
if self.n(state) > self.max:
|
| 196 |
+
state, _ = self.cut(state, self.max)
|
| 197 |
+
truncated = True
|
| 198 |
+
return state, truncated
|
| 199 |
+
|
| 200 |
+
|