Skip to content

Commit fc25e0e

Browse files
author
Ronald Tse
committed
feat(hf): Hugging Face org mirror pipeline - riboseinc/<id>
- model_card.py: models.yaml entry -> HF README (frontmatter, metrics table with provenance, curated benchmark block wrapper) - TDD, 8 tests - publish_hf.py: per-id CLI (assembled zip + generated card -> riboseinc/<id>), dry-run mode, HF_TOKEN from env, classes.json side path for plane artifacts - cards/ara-diac-2.0.md: the flagship curated block (SadeedDiac-25 leaderboard, architecture, provenance) User actions to go live: fine-grained HF token (write to the model repos) as the HF_TOKEN secret on this repo, then python3 scripts/publish_hf.py --id ara-diac-2.0 --zip <assembled>
1 parent 87f8992 commit fc25e0e

4 files changed

Lines changed: 290 additions & 0 deletions

File tree

‎cards/ara-diac-2.0.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## Benchmark
2+
3+
SadeedDiac-25 — full 1,200-paragraph set, Misraj's evaluator, zero
4+
skipped examples, greedy decode (protocol-matched published numbers
5+
only):
6+
7+
| system | Total DER | kind |
8+
|---|---|---|
9+
| Claude-3.7-Sonnet | 1.39% | frontier LLM |
10+
| **ara-diac-2.0 (this model)** | **2.29%** | dedicated, 580M |
11+
| GLM-5.2 | 2.69% | frontier LLM |
12+
| Gemini-Flash-2.0 | 3.19% | frontier LLM |
13+
| GPT-4 | 3.86% | frontier LLM |
14+
| Sadeed-1.5B | 7.29% | leaderboard baseline |
15+
16+
Word-final iʿrāb endings (the hardest component): 1.33% DER.
17+
Out-of-domain generalization (WikiNews-2024 multi-reference):
18+
17.38 WER / 11.83 DER.
19+
20+
## Architecture
21+
22+
ByT5-large-class seq2seq transformer (580M parameters), byte-level
23+
input/output (no tokenizer — id = byte + 3). Trained on classical and
24+
modern Arabic running text with a news-domain adaptation stage over a
25+
morphology-auxiliary curriculum. The canonical distribution is the
26+
Secryst IMF artifact (sha256-verified, parity-gated against the
27+
PyTorch reference); this repository mirrors those exact bytes.

‎scripts/model_card.py‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""HF model-card generator: a models.yaml entry becomes the README.md
2+
model card for riboseinc/<id> (the org mirror of the index id).
3+
4+
Curated benchmark/prose blocks live in cards/<id>.md and are wrapped
5+
by render_card; entries without a curated block get the metrics table
6+
alone. The index remains the single source of truth for numbers.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
HF_ORG = "riboseinc"
12+
13+
_LANG_BY_PREFIX = {
14+
"ara": "ar", "heb": "he", "fas": "fa", "tha": "th", "urd": "ur",
15+
"khm": "km",
16+
}
17+
18+
_GIB = 1024 ** 3
19+
20+
21+
def hf_repo_for(model_id: str) -> str:
22+
return f"{HF_ORG}/{model_id}"
23+
24+
25+
def _fmt_size(size: int | None) -> str:
26+
if not size:
27+
return ""
28+
return f"{size / _GIB:.2f} GiB"
29+
30+
31+
def render_card(model_id: str, entry: dict, variants: list[str],
32+
body: str = "", org: str = HF_ORG) -> str:
33+
lang = _LANG_BY_PREFIX.get(model_id.split("-")[0], "multilingual")
34+
lic = str(entry.get("license", "BSD-3-Clause")).lower()
35+
size = _fmt_size(entry.get("size"))
36+
lines = [
37+
"---",
38+
"license: " + lic,
39+
"language: " + lang,
40+
"inference: false",
41+
"tags:",
42+
" - secryst",
43+
" - transliteration",
44+
]
45+
task = entry.get("task", "")
46+
if task == "diacritization":
47+
lines.append(" - diacritization")
48+
elif task == "g2p":
49+
lines.append(" - grapheme-to-phoneme")
50+
lines += [
51+
" - onnx",
52+
" - byt5",
53+
"---",
54+
"",
55+
f"# {model_id}",
56+
"",
57+
f"Part of the [Secryst](https://secryst.github.io) phonological-layer "
58+
f"family — a dedicated model for **{task}** ({lang.upper()}), distributed "
59+
f"as a Secryst IMF artifact and loadable through any Secryst crystal:",
60+
"",
61+
"```python",
62+
"from secryst import Model",
63+
f"Model.load(\"{model_id}\")",
64+
"```",
65+
"",
66+
]
67+
if size:
68+
lines += [f"- Artifact: `{entry.get('filename', '')}` ({size})"]
69+
if variants:
70+
lines += ["- Precision variants: " + ", ".join(f"`{v}`" for v in variants)]
71+
metrics = entry.get("metrics") or []
72+
if metrics:
73+
lines += [
74+
"",
75+
"## Measured quality",
76+
"",
77+
"Full evaluation sets, zero skipped examples, the benchmark's own evaluator. "
78+
"Provenance for every number is the results record of the training project.",
79+
"",
80+
"| metric | value | provenance |",
81+
"|---|---|---|",
82+
]
83+
for m in metrics:
84+
prov = m.get("source", "")
85+
lines.append(f"| {m['name']} | {m['value']} | {prov} |")
86+
if body:
87+
lines += ["", body]
88+
lines += [
89+
"",
90+
"## License",
91+
"",
92+
lic.upper(),
93+
"",
94+
"## Provenance",
95+
"",
96+
"Trained in the [Secryst training monorepo](https://github.com/secryst/secryst-train) "
97+
"and published through the [interscript-ml](https://github.com/interscript/interscript-ml) "
98+
"distribution contract (models.yaml index, sha256-verified release assets, "
99+
"parity-gated artifacts). The canonical download channel is GitHub Releases; "
100+
f"this repository is the Hugging Face mirror of the same bytes, pinned to the "
101+
f"index revision.",
102+
]
103+
return "\n".join(lines) + "\n"

‎scripts/publish_hf.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Publish a models.yaml entry to the Hugging Face org mirror.
2+
3+
One command, both channels: the GitHub Releases asset stays the
4+
runtime channel; this adds the HF mirror (riboseinc/<id>) with a
5+
generated model card.
6+
7+
Usage:
8+
python3 scripts/publish_hf.py --id ara-diac-2.0 --zip /path/to/ara-diac-2.0-fp32.zip --dry-run
9+
python3 scripts/publish_hf.py --id ara-diac-2.0 --zip /path/... # uploads; needs HF_TOKEN
10+
11+
--zip accepts the assembled zip (parts reassembled: cat *.part-* > zip).
12+
Variant ids (e.g. ara-diac-2.0-int8) are separate repos: run once per id.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
import os
19+
import sys
20+
from pathlib import Path
21+
22+
import yaml
23+
24+
sys.path.insert(0, str(Path(__file__).resolve().parent))
25+
from model_card import hf_repo_for, render_card # noqa: E402
26+
27+
INDEX = "models.yaml"
28+
CARDS_DIR = Path(__file__).resolve().parent.parent / "cards"
29+
30+
31+
def load_entry(model_id: str) -> dict:
32+
index = yaml.safe_load(Path(INDEX).read_text(encoding="utf-8"))["models"]
33+
entry = index.get(model_id)
34+
if not entry:
35+
raise SystemExit(f"{model_id} not in {INDEX}")
36+
return entry
37+
38+
39+
def variants_of(model_id: str, index: dict) -> list[str]:
40+
return [k for k in index if k != model_id and k.startswith(model_id + "-")]
41+
42+
43+
def curated_body(model_id: str) -> str:
44+
card = CARDS_DIR / f"{model_id}.md"
45+
return card.read_text(encoding="utf-8") if card.exists() else ""
46+
47+
48+
def main() -> None:
49+
ap = argparse.ArgumentParser()
50+
ap.add_argument("--id", required=True)
51+
ap.add_argument("--zip", required=True, type=Path)
52+
ap.add_argument("--dry-run", action="store_true")
53+
args = ap.parse_args()
54+
55+
index = yaml.safe_load(Path(INDEX).read_text(encoding="utf-8"))["models"]
56+
entry = load_entry(args.id)
57+
repo = hf_repo_for(args.id)
58+
card = render_card(args.id, entry, variants_of(args.id, index),
59+
body=curated_body(args.id))
60+
zipped = args.zip.read_bytes()
61+
62+
print(f"repo: {repo}")
63+
print(f"artifact: {args.zip} ({len(zipped)} bytes; index sha256 "
64+
f"{entry.get('sha256', '?')[:16]}…)")
65+
print(f"card: {len(card)} chars, {len(card.splitlines())} lines")
66+
if args.dry_run:
67+
print(card)
68+
print("[dry-run] no upload performed")
69+
return
70+
71+
token = os.environ.get("HF_TOKEN")
72+
if not token:
73+
raise SystemExit("HF_TOKEN not set (fine-grained, write-scoped to the repo)")
74+
from huggingface_hub import HfApi
75+
76+
api = HfApi(token=token)
77+
api.create_repo(repo, repo_type="model", private=False, exist_ok=True)
78+
api.upload_file(path_or_fileobj=card.encode("utf-8"),
79+
path_in_repo="README.md", repo_id=repo, repo_type="model")
80+
api.upload_file(path_or_fileobj=str(args.zip),
81+
path_in_repo=entry.get("filename", args.zip.name),
82+
repo_id=repo, repo_type="model")
83+
classes = args.zip.with_name("classes.json")
84+
if classes.exists(): # plane artifacts carry the class inventory
85+
api.upload_file(path_or_fileobj=str(classes),
86+
path_in_repo="classes.json", repo_id=repo, repo_type="model")
87+
print(f"uploaded https://huggingface.co/{repo}")
88+
89+
90+
if __name__ == "__main__":
91+
main()

‎tests/test_model_card.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Tests for the HF model-card generator (models.yaml -> README.md)."""
2+
3+
import sys
4+
import unittest
5+
from pathlib import Path
6+
7+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
8+
9+
from model_card import hf_repo_for, render_card
10+
11+
ENTRY = {
12+
"task": "diacritization",
13+
"precision": "fp32",
14+
"filename": "ara-diac-2.0-fp32.zip",
15+
"size": 2780299051,
16+
"metrics": [
17+
{"name": "der_total_greedy", "value": 2.2864,
18+
"source": "rababa/docs/RESULTS.md#r7-verdict"},
19+
{"name": "der_morph_greedy", "value": 1.3343,
20+
"source": "rababa/docs/RESULTS.md#r7-verdict"},
21+
],
22+
"license": "BSD-3-Clause",
23+
}
24+
VARIANTS = ["ara-diac-2.0-int8"]
25+
26+
27+
class TestHfRepo(unittest.TestCase):
28+
def test_repo_is_org_plus_index_id(self):
29+
self.assertEqual(hf_repo_for("ara-diac-2.0"), "riboseinc/ara-diac-2.0")
30+
31+
def test_variant_ids_keep_their_own_repo(self):
32+
self.assertEqual(hf_repo_for("ara-diac-small-2.1-int8"),
33+
"riboseinc/ara-diac-small-2.1-int8")
34+
35+
36+
class TestRenderCard(unittest.TestCase):
37+
def test_frontmatter_fields(self):
38+
card = render_card("ara-diac-2.0", ENTRY, VARIANTS, body="## Benchmark\n\n2.29% DER.")
39+
self.assertIn("license: bsd-3-clause", card)
40+
self.assertIn("language: ar", card)
41+
self.assertIn("inference: false", card)
42+
self.assertIn("secryst", card)
43+
44+
def test_metrics_table_with_provenance(self):
45+
card = render_card("ara-diac-2.0", ENTRY, VARIANTS, body="")
46+
self.assertIn("| der_total_greedy | 2.2864 |", card)
47+
self.assertIn("rababa/docs/RESULTS.md#r7-verdict", card)
48+
49+
def test_task_and_size(self):
50+
card = render_card("ara-diac-2.0", ENTRY, VARIANTS, body="")
51+
self.assertIn("diacritization", card)
52+
self.assertIn("2.59 GiB", card)
53+
54+
def test_variants_listed(self):
55+
card = render_card("ara-diac-2.0", ENTRY, VARIANTS, body="")
56+
self.assertIn("ara-diac-2.0-int8", card)
57+
58+
def test_body_included(self):
59+
card = render_card("ara-diac-2.0", ENTRY, VARIANTS, body="## Benchmark\n\n2.29% DER.")
60+
self.assertIn("## Benchmark", card)
61+
self.assertIn("2.29% DER.", card)
62+
63+
def test_language_mapping_per_task_language(self):
64+
card = render_card("tha-g2p-base-1.0", {**ENTRY, "task": "g2p"}, [], body="")
65+
self.assertIn("language: th", card)
66+
67+
68+
if __name__ == "__main__":
69+
unittest.main()

0 commit comments

Comments
 (0)