146 lines
5.1 KiB
Python
146 lines
5.1 KiB
Python
"""Build the Hugging Face model card for a packaged pipeline.
|
|
|
|
`spacy package` already writes a README into the wheel, and `spacy huggingface-hub push`
|
|
uploads it as the card. That card is a metadata dump: no install line, no usage, no
|
|
throughput, and no YAML frontmatter, so the Hub cannot index the model by language or task.
|
|
|
|
This composes a card from the same sources of truth (`meta.json` and the JSON written by
|
|
scripts/benchmark_throughput.py) rather than from hand-copied numbers, so the card cannot
|
|
drift from the artifact it describes.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
# meta.json key -> (row label, reference note). Only keys the pipeline actually evidences
|
|
# are emitted; a missing key means the corpus could not score it.
|
|
METRICS = [
|
|
("token_acc", "Tokenization accuracy", ""),
|
|
("tag_acc", "XPOS tag accuracy", ""),
|
|
("pos_acc", "UPOS tag accuracy", ""),
|
|
("morph_acc", "Morphological features", ""),
|
|
("lemma_acc", "Lemma accuracy", ""),
|
|
("dep_uas", "Unlabelled attachment (UAS)", ""),
|
|
("dep_las", "Labelled attachment (LAS)", ""),
|
|
("sents_f", "Sentence segmentation F", ""),
|
|
("ents_p", "NER precision", ""),
|
|
("ents_r", "NER recall", ""),
|
|
("ents_f", "NER F-score", ""),
|
|
]
|
|
|
|
|
|
def load(path):
|
|
return json.loads(Path(path).read_text())
|
|
|
|
|
|
def throughput_rows(paths):
|
|
rows = []
|
|
for p in paths:
|
|
if not Path(p).exists():
|
|
continue
|
|
d = load(p)
|
|
rows.append((d["device"], d["batch_size"], d["wps_median"]))
|
|
return rows
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--meta", required=True, help="meta.json of the finalized pipeline")
|
|
ap.add_argument("--throughput", nargs="*", default=[], help="benchmark_throughput JSONs")
|
|
ap.add_argument("--repo-id", required=True, help="e.g. Phazel/fa_core_news_trf")
|
|
ap.add_argument("--wheel-name", required=True)
|
|
ap.add_argument("--out", required=True)
|
|
args = ap.parse_args()
|
|
|
|
meta = load(args.meta)
|
|
name = f"{meta['lang']}_{meta['name']}"
|
|
perf = meta.get("performance", {})
|
|
|
|
lines = []
|
|
# Frontmatter: without this the Hub cannot filter the model by language or library.
|
|
lines += [
|
|
"---",
|
|
"language:",
|
|
"- fa",
|
|
f"license: {meta.get('license', 'cc-by-sa-4.0').lower().replace(' ', '-')}",
|
|
"library_name: spacy",
|
|
"pipeline_tag: token-classification",
|
|
"tags:",
|
|
"- spacy",
|
|
"- token-classification",
|
|
"- persian",
|
|
"- farsi",
|
|
"---",
|
|
"",
|
|
f"# {name}",
|
|
"",
|
|
meta.get("description", "").strip(),
|
|
"",
|
|
]
|
|
|
|
lines += [
|
|
"## Install",
|
|
"",
|
|
"```bash",
|
|
f"pip install https://huggingface.co/{args.repo_id}/resolve/main/{args.wheel_name}",
|
|
"```",
|
|
"",
|
|
"```python",
|
|
"import spacy",
|
|
f'nlp = spacy.load("{name}")',
|
|
'doc = nlp("شرکت ایران خودرو اعلام کرد که تولید خود را افزایش می\u200cدهد.")',
|
|
"print([(t.text, t.pos_, t.lemma_, t.dep_) for t in doc])",
|
|
"print([(e.text, e.label_) for e in doc.ents])",
|
|
"```",
|
|
"",
|
|
]
|
|
|
|
lines += ["## Accuracy", "",
|
|
"Scored with `spacy benchmark accuracy` on the held-out PerDT test split.",
|
|
"", "| Metric | Score |", "| --- | ---: |"]
|
|
for key, label, _ in METRICS:
|
|
v = perf.get(key)
|
|
if isinstance(v, (int, float)):
|
|
lines.append(f"| {label} | {v * 100:.2f} |")
|
|
lines.append("")
|
|
|
|
rows = throughput_rows(args.throughput)
|
|
if rows:
|
|
lines += ["## Throughput", "",
|
|
"Median of repeated `nlp.pipe` passes over the 146-document PerDT test",
|
|
"split (23,825 tokens), timing the pipe only. Warmup pass discarded.",
|
|
"", "| Device | Batch | Words/s |", "| --- | ---: | ---: |"]
|
|
for device, batch, wps in rows:
|
|
lines.append(f"| {device} | {batch} | {wps:,.0f} |")
|
|
lines.append("")
|
|
gpu = next((r for r in rows if r[0].startswith("gpu")), None)
|
|
cpu = next((r for r in rows if r[0].startswith("cpu")), None)
|
|
if gpu and cpu:
|
|
lines += [
|
|
f"A transformer pipeline is GPU-bound: the T4 is {gpu[2] / cpu[2]:.0f}x the "
|
|
f"CPU on the same machine. On CPU this runs roughly 25x slower than the "
|
|
f"`sm`/`md`/`lg` tiers, which is the price of the accuracy below.",
|
|
"",
|
|
]
|
|
|
|
lines += ["## Sources", "", "| Source | Author | Licence |", "| --- | --- | --- |"]
|
|
for s in meta.get("sources", []):
|
|
url, nm = s.get("url"), s.get("name", "")
|
|
label = f"[{nm}]({url})" if url else nm
|
|
lines.append(f"| {label} | {s.get('author', '')} | {s.get('license', '')} |")
|
|
lines.append("")
|
|
|
|
notes = (meta.get("notes") or "").strip()
|
|
if notes:
|
|
lines += ["## Notes", "", notes, ""]
|
|
|
|
out = Path(args.out)
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_text("\n".join(lines), encoding="utf-8")
|
|
print(f"wrote {out} ({out.stat().st_size} bytes)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|