BHARGAV REDDY commited on
Commit
7118d38
·
verified ·
1 Parent(s): ab3dc07

Upload ASTERIZER runnable bundle

Browse files
Files changed (2) hide show
  1. asterizer.py +179 -15
  2. sources.lock.json +38 -47
asterizer.py CHANGED
@@ -104,10 +104,16 @@ import shutil
104
  import sys
105
  import time
106
  import unicodedata
 
107
  from collections import Counter
108
  from pathlib import Path
109
 
110
  ROOT = Path(__file__).resolve().parent
 
 
 
 
 
111
 
112
  # ==================================================================================
113
  # CONFIG (was config/corpus_config.yaml - edit here)
@@ -204,13 +210,14 @@ BUCKETS = [
204
  {"dataset": "ai4bharat/sangraha", "config_dir": "verified/tel", "lang": "te", "script": "Telu", "gb": 5.8, "license": SANGRAHA_LIC},
205
  {"dataset": "ai4bharat/sangraha", "config_dir": "verified/mal", "lang": "ml", "script": "Mlym", "gb": 5.8, "license": SANGRAHA_LIC},
206
  ]},
207
- {"name": "other_indic", "target_gb": 4.8, "sources": [ # hi/bn + Gujarati/Marathi/Punjabi/Odia
208
- {"dataset": "ai4bharat/sangraha", "config_dir": "verified/hin", "lang": "hi", "script": "Deva", "gb": 1.4, "license": SANGRAHA_LIC},
209
- {"dataset": "ai4bharat/sangraha", "config_dir": "verified/ben", "lang": "bn", "script": "Beng", "gb": 1.0, "license": SANGRAHA_LIC},
210
- {"dataset": "ai4bharat/sangraha", "config_dir": "verified/guj", "lang": "gu", "script": "Gujr", "gb": 0.6, "license": SANGRAHA_LIC},
211
- {"dataset": "ai4bharat/sangraha", "config_dir": "verified/mar", "lang": "mr", "script": "Deva", "gb": 0.6, "license": SANGRAHA_LIC},
212
- {"dataset": "ai4bharat/sangraha", "config_dir": "verified/pan", "lang": "pa", "script": "Guru", "gb": 0.6, "license": SANGRAHA_LIC},
213
- {"dataset": "ai4bharat/sangraha", "config_dir": "verified/ory", "lang": "or", "script": "Orya", "gb": 0.6, "license": SANGRAHA_LIC},
 
214
  ]},
215
  {"name": "code", "target_gb": 9.9, "min_frac": 0.5, "sources": [
216
  # Code for the freeze. StarCoderData is BigCode's permissive-filtered set, but its license tag
@@ -284,7 +291,8 @@ BUCKETS = [
284
  {"name": "african", "target_gb": 0.9, "min_frac": 0.4, "sources": [ # Swahili/Amharic/Yoruba/Hausa
285
  {"dataset": "HuggingFaceFW/fineweb-2", "name": "swh_Latn", "lang": "sw", "script": "Latn", "gb": 0.3, "license": ODCBY_LIC},
286
  {"dataset": "HuggingFaceFW/fineweb-2", "name": "amh_Ethi", "lang": "am", "script": "Ethi", "gb": 0.2, "license": ODCBY_LIC},
287
- {"dataset": "HuggingFaceFW/fineweb-2", "name": "yor_Latn", "lang": "yo", "script": "Latn", "gb": 0.2, "license": ODCBY_LIC},
 
288
  {"dataset": "HuggingFaceFW/fineweb-2", "name": "hau_Latn", "lang": "ha", "script": "Latn", "gb": 0.2, "license": ODCBY_LIC},
289
  ]},
290
  {"name": "coverage", "target_gb": 0.3, "min_frac": 0.3, "sources": [ # Tibetan/Sinhala (pure script coverage)
@@ -385,6 +393,7 @@ def _config_fingerprint(cfg) -> str:
385
  "sources": [{"dataset": s["dataset"],
386
  "config": s.get("config_dir") or s.get("name") or "",
387
  "gb": s.get("gb"), "license": s.get("license"),
 
388
  "revision": s.get("revision")} for s in b["sources"]]}
389
  for b in cfg["buckets"]],
390
  }
@@ -681,9 +690,12 @@ def stage_download(cfg, fingerprint=None, force=False):
681
  if stgt < 0.1 * (1024 ** 3): # exempt tiny coverage-only sources from the per-source floor
682
  continue
683
  sgot = stats.get(f"{s['dataset']}|{s.get('config_dir') or s.get('name') or ''}", 0)
684
- if sgot < SOURCE_MIN_FRAC * stgt:
 
 
 
685
  lbl = s.get("config_dir") or s.get("name") or s["dataset"]
686
- bad.append(f"({bucket['name']}/{lbl}): {human(sgot)} < {int(SOURCE_MIN_FRAC * 100)}% of {human(stgt)} (per-source)")
687
  if bad:
688
  (raw_dir / f"{bucket['name']}.done").unlink(missing_ok=True) # drop bad checkpoint
689
  short.extend(bad)
@@ -1673,6 +1685,120 @@ def stage_extend(base_path, new_data, num_new, out_path, min_frequency=5):
1673
  base.save(out_path)
1674
  print(f"added {added} tokens (append-only GROWTH) -> {out_path}; vocab {base.get_vocab_size():,}")
1675
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1676
  # ==================================================================================
1677
  # resolve + orchestrate
1678
  # ==================================================================================
@@ -1703,7 +1829,7 @@ def resolve_data(target_gb, jobs, lockfile=None, freeze=False):
1703
  pinned, unpinned = apply_lock(buckets, lock)
1704
  else:
1705
  pinned = unpinned = 0
1706
- data_dir = ROOT / "runs" / f"data_{_gb_tag(target_gb)}"
1707
  return {
1708
  "seed": SEED, "jobs": jobs, "buckets": buckets,
1709
  "cleaning": dict(CLEANING, num_workers=jobs),
@@ -1723,7 +1849,7 @@ def resolve_tok(vocab_key, target_gb, corpus_file, jobs, min_freq=None):
1723
  tok["output_name"] = f"asterizer_{vocab_key}_v1"
1724
  if min_freq:
1725
  tok["min_frequency"] = int(min_freq)
1726
- run_dir = ROOT / "runs" / f"{vocab_key}_{_gb_tag(target_gb)}"
1727
  return {
1728
  "seed": SEED, "jobs": jobs, "tokenizer": tok,
1729
  "corpus_file": str(corpus_file),
@@ -1780,7 +1906,8 @@ def _smoke_gate(tok_path):
1780
  if not stage_smoke(tok_path, show_tokens=True):
1781
  sys.exit("\nFREEZE GATE FAILED: case/round-trip/compat tests did not pass -- NOT frozen.")
1782
 
1783
- def run_build(vocabs, gb, jobs, yes, force, dry_run, eval_full=True, allow_review=False):
 
1784
  lockfile = None
1785
  lock = load_lock(lockfile)
1786
  data_cfg, data_dir = resolve_data(gb, jobs, lockfile, freeze=require_lock)
@@ -1811,6 +1938,12 @@ def run_build(vocabs, gb, jobs, yes, force, dry_run, eval_full=True, allow_revie
1811
  print(" code src : using ungated `codeparrot/codeparrot-clean` dev fallback (non-freeze validation build)")
1812
  print(f" eval : {'held-out + baselines (cl100k/o200k/Llama-3/Qwen/Gemma/Sarvam)' if eval_full else 'basic only'}")
1813
  print(f" mode : {'FORCE rebuild' if force else 'resume (checkpointed)'}")
 
 
 
 
 
 
1814
  print("------------------------------------------------------------")
1815
  if dry_run:
1816
  print("[dry-run] plan only.")
@@ -1843,6 +1976,8 @@ def run_build(vocabs, gb, jobs, yes, force, dry_run, eval_full=True, allow_revie
1843
  data_dir.mkdir(parents=True, exist_ok=True)
1844
  data_fp = _config_fingerprint(data_cfg)
1845
  data_cfg["fingerprint"] = data_fp
 
 
1846
  dst = _state(data_dir)
1847
  dst["config"] = {"gb": gb, "jobs": jobs, "fingerprint": data_fp}
1848
  _save_state(data_dir, dst)
@@ -1852,12 +1987,23 @@ def run_build(vocabs, gb, jobs, yes, force, dry_run, eval_full=True, allow_revie
1852
  def _ns():
1853
  _step[0] += 1
1854
  return _step[0]
 
 
 
 
 
1855
  run_stage(data_dir, dst, "download", lambda: stage_download(data_cfg, data_fp, force), force, data_fp,
1856
  step=_ns(), total=total_stages)
 
 
1857
  run_stage(data_dir, dst, "clean", lambda: stage_clean(data_cfg, data_fp, force), force, data_fp,
1858
  step=_ns(), total=total_stages)
 
 
1859
  run_stage(data_dir, dst, "corpus", lambda: stage_corpus(data_cfg), force, data_fp,
1860
  outputs=[corpus_file, str(Path(data_dir) / "manifest.json")], step=_ns(), total=total_stages)
 
 
1861
 
1862
  # ---- train + validate each vocab on the shared corpus ----
1863
  results = []
@@ -1904,7 +2050,7 @@ def run_build(vocabs, gb, jobs, yes, force, dry_run, eval_full=True, allow_revie
1904
  print("=" * 60)
1905
 
1906
  def main():
1907
- global require_lock, min_frequency # declare globals used in run_build
1908
  ap = argparse.ArgumentParser(description="ASTERIZER tokenizer - single-file builder")
1909
  ap.add_argument("--vocab", help="profile(s), comma-separated e.g. 64K,128K")
1910
  ap.add_argument("--gb", type=float)
@@ -1928,6 +2074,13 @@ def main():
1928
  ap.add_argument("--evaldir", help="directory of local held-out text files (<name>.txt/.jsonl) for --eval")
1929
  ap.add_argument("--no-eval-full", action="store_true", help="skip the held-out/baseline fertility comparison after each build (default on)")
1930
  ap.add_argument("--allow-license-review", action="store_true", help="explicitly accept review/non-approved source licenses (recorded in manifest.json)")
 
 
 
 
 
 
 
1931
  args = ap.parse_args()
1932
 
1933
  if args.lock:
@@ -1953,6 +2106,11 @@ def main():
1953
  stage_extend(args.extend, args.new_data, args.num_new, args.out)
1954
  return
1955
 
 
 
 
 
 
1956
  cpu = os.cpu_count() or 4
1957
  jobs = args.jobs if args.jobs and args.jobs > 0 else cpu
1958
  print("=" * 60)
@@ -1961,6 +2119,7 @@ def main():
1961
 
1962
  require_lock = args.require_lock
1963
  min_frequency = args.min_freq
 
1964
 
1965
  if args.vocab:
1966
  vocabs = [v.strip() for v in args.vocab.split(",") if v.strip()]
@@ -1999,8 +2158,13 @@ def main():
1999
  else:
2000
  gb = float(choice)
2001
 
 
 
 
2002
  run_build(vocabs, gb, jobs, args.yes, args.force, args.dry_run,
2003
- eval_full=not args.no_eval_full, allow_review=args.allow_license_review)
 
 
2004
 
2005
  if __name__ == "__main__":
2006
  main()
 
104
  import sys
105
  import time
106
  import unicodedata
107
+ import zipfile
108
  from collections import Counter
109
  from pathlib import Path
110
 
111
  ROOT = Path(__file__).resolve().parent
112
+ runs_root_override = None
113
+
114
+ def runs_root():
115
+ base = runs_root_override or os.environ.get("ASTERIZER_RUNS_ROOT")
116
+ return Path(base).resolve() if base else (ROOT / "runs")
117
 
118
  # ==================================================================================
119
  # CONFIG (was config/corpus_config.yaml - edit here)
 
210
  {"dataset": "ai4bharat/sangraha", "config_dir": "verified/tel", "lang": "te", "script": "Telu", "gb": 5.8, "license": SANGRAHA_LIC},
211
  {"dataset": "ai4bharat/sangraha", "config_dir": "verified/mal", "lang": "ml", "script": "Mlym", "gb": 5.8, "license": SANGRAHA_LIC},
212
  ]},
213
+ {"name": "other_indic", "target_gb": 4.8, "sources": [ # hi/bn + Gujarati/Marathi/Punjabi
214
+ # `verified/ory` is absent at the pinned Sangraha revision, so redistribute its quota
215
+ # across the remaining sources instead of making the freeze fail on a dead upstream path.
216
+ {"dataset": "ai4bharat/sangraha", "config_dir": "verified/hin", "lang": "hi", "script": "Deva", "gb": 1.52, "license": SANGRAHA_LIC},
217
+ {"dataset": "ai4bharat/sangraha", "config_dir": "verified/ben", "lang": "bn", "script": "Beng", "gb": 1.12, "license": SANGRAHA_LIC},
218
+ {"dataset": "ai4bharat/sangraha", "config_dir": "verified/guj", "lang": "gu", "script": "Gujr", "gb": 0.72, "license": SANGRAHA_LIC},
219
+ {"dataset": "ai4bharat/sangraha", "config_dir": "verified/mar", "lang": "mr", "script": "Deva", "gb": 0.72, "license": SANGRAHA_LIC},
220
+ {"dataset": "ai4bharat/sangraha", "config_dir": "verified/pan", "lang": "pa", "script": "Guru", "gb": 0.72, "license": SANGRAHA_LIC},
221
  ]},
222
  {"name": "code", "target_gb": 9.9, "min_frac": 0.5, "sources": [
223
  # Code for the freeze. StarCoderData is BigCode's permissive-filtered set, but its license tag
 
291
  {"name": "african", "target_gb": 0.9, "min_frac": 0.4, "sources": [ # Swahili/Amharic/Yoruba/Hausa
292
  {"dataset": "HuggingFaceFW/fineweb-2", "name": "swh_Latn", "lang": "sw", "script": "Latn", "gb": 0.3, "license": ODCBY_LIC},
293
  {"dataset": "HuggingFaceFW/fineweb-2", "name": "amh_Ethi", "lang": "am", "script": "Ethi", "gb": 0.2, "license": ODCBY_LIC},
294
+ # Yoruba tends to saturate below the global 40% source floor at the pinned revision.
295
+ {"dataset": "HuggingFaceFW/fineweb-2", "name": "yor_Latn", "lang": "yo", "script": "Latn", "gb": 0.2, "license": ODCBY_LIC, "min_source_frac": 0.35},
296
  {"dataset": "HuggingFaceFW/fineweb-2", "name": "hau_Latn", "lang": "ha", "script": "Latn", "gb": 0.2, "license": ODCBY_LIC},
297
  ]},
298
  {"name": "coverage", "target_gb": 0.3, "min_frac": 0.3, "sources": [ # Tibetan/Sinhala (pure script coverage)
 
393
  "sources": [{"dataset": s["dataset"],
394
  "config": s.get("config_dir") or s.get("name") or "",
395
  "gb": s.get("gb"), "license": s.get("license"),
396
+ "min_source_frac": s.get("min_source_frac"),
397
  "revision": s.get("revision")} for s in b["sources"]]}
398
  for b in cfg["buckets"]],
399
  }
 
690
  if stgt < 0.1 * (1024 ** 3): # exempt tiny coverage-only sources from the per-source floor
691
  continue
692
  sgot = stats.get(f"{s['dataset']}|{s.get('config_dir') or s.get('name') or ''}", 0)
693
+ source_floor = float(s.get("min_source_frac", SOURCE_MIN_FRAC))
694
+ if source_floor <= 0:
695
+ continue
696
+ if sgot < source_floor * stgt:
697
  lbl = s.get("config_dir") or s.get("name") or s["dataset"]
698
+ bad.append(f"({bucket['name']}/{lbl}): {human(sgot)} < {int(source_floor * 100)}% of {human(stgt)} (per-source)")
699
  if bad:
700
  (raw_dir / f"{bucket['name']}.done").unlink(missing_ok=True) # drop bad checkpoint
701
  short.extend(bad)
 
1685
  base.save(out_path)
1686
  print(f"added {added} tokens (append-only GROWTH) -> {out_path}; vocab {base.get_vocab_size():,}")
1687
 
1688
+ # ==================================================================================
1689
+ # prepared-data transfer helpers
1690
+ # ==================================================================================
1691
+
1692
+ def _copy_tree_contents(src, dst):
1693
+ dst.mkdir(parents=True, exist_ok=True)
1694
+ for item in src.iterdir():
1695
+ target = dst / item.name
1696
+ if item.is_dir():
1697
+ shutil.copytree(item, target, dirs_exist_ok=True)
1698
+ else:
1699
+ shutil.copy2(item, target)
1700
+
1701
+ def _find_prepared_root(base, expected_name):
1702
+ base = Path(base)
1703
+ probes = [base, base / expected_name]
1704
+ probes.extend(p for p in base.iterdir() if p.is_dir())
1705
+ for p in probes:
1706
+ if not p.exists() or not p.is_dir():
1707
+ continue
1708
+ if (p / "raw").is_dir() or (p / "clean").is_dir() or (p / "corpus.jsonl").exists():
1709
+ return p
1710
+ sys.exit(f"[prepared] could not find a data bundle root under {base}")
1711
+
1712
+ def _zip_extract_root(zf, expected_name):
1713
+ tops = {name.split("/", 1)[0] for name in zf.namelist() if name and not name.endswith("/")}
1714
+ return expected_name if expected_name in tops else None
1715
+
1716
+ def import_prepared_data(data_dir, prepared_dir=None, prepared_zip=None, force=False):
1717
+ if bool(prepared_dir) == bool(prepared_zip):
1718
+ return False
1719
+ data_dir = Path(data_dir)
1720
+ if force and data_dir.exists():
1721
+ shutil.rmtree(data_dir)
1722
+ if data_dir.exists() and any(data_dir.iterdir()) and not force:
1723
+ print(f"[prepared] {data_dir} already populated - keeping existing files")
1724
+ return False
1725
+ if prepared_dir:
1726
+ src = _find_prepared_root(Path(prepared_dir).resolve(), data_dir.name)
1727
+ if src == data_dir.resolve():
1728
+ print(f"[prepared] using in-place prepared data at {src}")
1729
+ return True
1730
+ print(f"[prepared] importing directory {src} -> {data_dir}")
1731
+ _copy_tree_contents(src, data_dir)
1732
+ return True
1733
+ zpath = Path(prepared_zip).resolve()
1734
+ with zipfile.ZipFile(zpath) as zf:
1735
+ zip_root = _zip_extract_root(zf, data_dir.name)
1736
+ target = data_dir.parent if zip_root else data_dir
1737
+ target.mkdir(parents=True, exist_ok=True)
1738
+ print(f"[prepared] extracting {zpath} -> {target}")
1739
+ zf.extractall(target)
1740
+ return True
1741
+
1742
+ def sync_prepared_state(data_dir, cfg, fingerprint, gb, jobs):
1743
+ data_dir = Path(data_dir)
1744
+ raw_dir = data_dir / "raw"
1745
+ clean_dir = data_dir / "clean"
1746
+ corpus_path = Path(cfg["corpus_file"])
1747
+ bucket_names = [b["name"] for b in cfg["buckets"]]
1748
+ st = _state(data_dir)
1749
+ st["config"] = {"gb": gb, "jobs": jobs, "fingerprint": fingerprint}
1750
+
1751
+ raw_ok = raw_dir.is_dir() and all((raw_dir / f"{b}.done").exists() and (raw_dir / f"{b}.jsonl").exists()
1752
+ for b in bucket_names)
1753
+ clean_ok = clean_dir.is_dir() and (clean_dir / "_clean_stats.json").exists() and all(
1754
+ (clean_dir / f"{b}.done").exists() and (clean_dir / f"{b}.jsonl").exists() for b in bucket_names
1755
+ )
1756
+ corpus_ok = corpus_path.exists() and (data_dir / "manifest.json").exists()
1757
+
1758
+ if raw_ok:
1759
+ st["stages"]["download"] = {"status": "done", "fingerprint": fingerprint, "seconds": 0}
1760
+ elif raw_dir.exists():
1761
+ print("[prepared] raw bundle is partial - download will resume missing buckets")
1762
+ if clean_ok:
1763
+ st["stages"]["clean"] = {"status": "done", "fingerprint": fingerprint, "seconds": 0}
1764
+ elif clean_dir.exists():
1765
+ print("[prepared] clean bundle is partial - clean will resume missing buckets")
1766
+ if corpus_ok:
1767
+ st["stages"]["corpus"] = {"status": "done", "fingerprint": fingerprint, "seconds": 0}
1768
+ _save_state(data_dir, st)
1769
+
1770
+ bits = []
1771
+ if raw_ok:
1772
+ bits.append("download")
1773
+ if clean_ok:
1774
+ bits.append("clean")
1775
+ if corpus_ok:
1776
+ bits.append("corpus")
1777
+ if bits:
1778
+ print(f"[prepared] reusing completed stage(s): {', '.join(bits)}")
1779
+
1780
+ def stage_pack_data_bundle(data_dir, out_zip):
1781
+ src = Path(data_dir).resolve()
1782
+ if not src.is_dir():
1783
+ sys.exit(f"--pack-data needs an existing data directory: {src}")
1784
+ out = Path(out_zip).resolve()
1785
+ out.parent.mkdir(parents=True, exist_ok=True)
1786
+ files = [p for p in src.rglob("*") if p.is_file()]
1787
+ if not files:
1788
+ sys.exit(f"no files to pack in {src}")
1789
+ total = sum(p.stat().st_size for p in files)
1790
+ written = 0
1791
+ print(f"[pack] {len(files):,} files, {human(total)} -> {out}")
1792
+ with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=6) as zf:
1793
+ for i, path in enumerate(files, 1):
1794
+ arc = Path(src.name) / path.relative_to(src)
1795
+ zf.write(path, arcname=str(arc).replace("\\", "/"))
1796
+ written += path.stat().st_size
1797
+ if i == len(files) or i % max(1, len(files) // 50) == 0:
1798
+ pct = 100 * written / max(1, total)
1799
+ print(f" [pack] {i}/{len(files)} files ({pct:.1f}%)", flush=True)
1800
+ print(f"[pack] wrote {out}")
1801
+
1802
  # ==================================================================================
1803
  # resolve + orchestrate
1804
  # ==================================================================================
 
1829
  pinned, unpinned = apply_lock(buckets, lock)
1830
  else:
1831
  pinned = unpinned = 0
1832
+ data_dir = runs_root() / f"data_{_gb_tag(target_gb)}"
1833
  return {
1834
  "seed": SEED, "jobs": jobs, "buckets": buckets,
1835
  "cleaning": dict(CLEANING, num_workers=jobs),
 
1849
  tok["output_name"] = f"asterizer_{vocab_key}_v1"
1850
  if min_freq:
1851
  tok["min_frequency"] = int(min_freq)
1852
+ run_dir = runs_root() / f"{vocab_key}_{_gb_tag(target_gb)}"
1853
  return {
1854
  "seed": SEED, "jobs": jobs, "tokenizer": tok,
1855
  "corpus_file": str(corpus_file),
 
1906
  if not stage_smoke(tok_path, show_tokens=True):
1907
  sys.exit("\nFREEZE GATE FAILED: case/round-trip/compat tests did not pass -- NOT frozen.")
1908
 
1909
+ def run_build(vocabs, gb, jobs, yes, force, dry_run, eval_full=True, allow_review=False,
1910
+ prepared_data_dir=None, prepared_data_zip=None, stop_after=None):
1911
  lockfile = None
1912
  lock = load_lock(lockfile)
1913
  data_cfg, data_dir = resolve_data(gb, jobs, lockfile, freeze=require_lock)
 
1938
  print(" code src : using ungated `codeparrot/codeparrot-clean` dev fallback (non-freeze validation build)")
1939
  print(f" eval : {'held-out + baselines (cl100k/o200k/Llama-3/Qwen/Gemma/Sarvam)' if eval_full else 'basic only'}")
1940
  print(f" mode : {'FORCE rebuild' if force else 'resume (checkpointed)'}")
1941
+ if prepared_data_dir:
1942
+ print(f" prepared : directory import from {prepared_data_dir}")
1943
+ elif prepared_data_zip:
1944
+ print(f" prepared : zip import from {prepared_data_zip}")
1945
+ if stop_after:
1946
+ print(f" stop : after shared stage `{stop_after}`")
1947
  print("------------------------------------------------------------")
1948
  if dry_run:
1949
  print("[dry-run] plan only.")
 
1976
  data_dir.mkdir(parents=True, exist_ok=True)
1977
  data_fp = _config_fingerprint(data_cfg)
1978
  data_cfg["fingerprint"] = data_fp
1979
+ import_prepared_data(data_dir, prepared_dir=prepared_data_dir, prepared_zip=prepared_data_zip, force=force)
1980
+ sync_prepared_state(data_dir, data_cfg, data_fp, gb, jobs)
1981
  dst = _state(data_dir)
1982
  dst["config"] = {"gb": gb, "jobs": jobs, "fingerprint": data_fp}
1983
  _save_state(data_dir, dst)
 
1987
  def _ns():
1988
  _step[0] += 1
1989
  return _step[0]
1990
+ def _stop_here(name):
1991
+ if stop_after == name:
1992
+ print(f"\n[stop-after] stopping after shared stage `{name}` as requested.")
1993
+ return True
1994
+ return False
1995
  run_stage(data_dir, dst, "download", lambda: stage_download(data_cfg, data_fp, force), force, data_fp,
1996
  step=_ns(), total=total_stages)
1997
+ if _stop_here("download"):
1998
+ return
1999
  run_stage(data_dir, dst, "clean", lambda: stage_clean(data_cfg, data_fp, force), force, data_fp,
2000
  step=_ns(), total=total_stages)
2001
+ if _stop_here("clean"):
2002
+ return
2003
  run_stage(data_dir, dst, "corpus", lambda: stage_corpus(data_cfg), force, data_fp,
2004
  outputs=[corpus_file, str(Path(data_dir) / "manifest.json")], step=_ns(), total=total_stages)
2005
+ if _stop_here("corpus"):
2006
+ return
2007
 
2008
  # ---- train + validate each vocab on the shared corpus ----
2009
  results = []
 
2050
  print("=" * 60)
2051
 
2052
  def main():
2053
+ global require_lock, min_frequency, runs_root_override # declare globals used in run_build
2054
  ap = argparse.ArgumentParser(description="ASTERIZER tokenizer - single-file builder")
2055
  ap.add_argument("--vocab", help="profile(s), comma-separated e.g. 64K,128K")
2056
  ap.add_argument("--gb", type=float)
 
2074
  ap.add_argument("--evaldir", help="directory of local held-out text files (<name>.txt/.jsonl) for --eval")
2075
  ap.add_argument("--no-eval-full", action="store_true", help="skip the held-out/baseline fertility comparison after each build (default on)")
2076
  ap.add_argument("--allow-license-review", action="store_true", help="explicitly accept review/non-approved source licenses (recorded in manifest.json)")
2077
+ ap.add_argument("--runs-root", help="override the base runs directory (default: ./runs or ASTERIZER_RUNS_ROOT)")
2078
+ ap.add_argument("--prepared-data-dir", help="reuse an existing runs/data_* directory and resume from its completed stages")
2079
+ ap.add_argument("--prepared-data-zip", help="extract a ZIP of runs/data_* before resuming the build")
2080
+ ap.add_argument("--stop-after", choices=("download", "clean", "corpus"),
2081
+ help="stop after a shared data stage; useful for preparing a transferable data bundle")
2082
+ ap.add_argument("--pack-data", metavar="RUNS_DATA_DIR",
2083
+ help="zip an existing runs/data_* directory for transfer and exit")
2084
  args = ap.parse_args()
2085
 
2086
  if args.lock:
 
2106
  stage_extend(args.extend, args.new_data, args.num_new, args.out)
2107
  return
2108
 
2109
+ if args.pack_data:
2110
+ out_zip = args.out if args.out != "asterizer_v2.json" else f"{Path(args.pack_data).name}.zip"
2111
+ stage_pack_data_bundle(args.pack_data, out_zip)
2112
+ return
2113
+
2114
  cpu = os.cpu_count() or 4
2115
  jobs = args.jobs if args.jobs and args.jobs > 0 else cpu
2116
  print("=" * 60)
 
2119
 
2120
  require_lock = args.require_lock
2121
  min_frequency = args.min_freq
2122
+ runs_root_override = args.runs_root
2123
 
2124
  if args.vocab:
2125
  vocabs = [v.strip() for v in args.vocab.split(",") if v.strip()]
 
2158
  else:
2159
  gb = float(choice)
2160
 
2161
+ if args.prepared_data_dir and args.prepared_data_zip:
2162
+ sys.exit("use only one of --prepared-data-dir or --prepared-data-zip")
2163
+
2164
  run_build(vocabs, gb, jobs, args.yes, args.force, args.dry_run,
2165
+ eval_full=not args.no_eval_full, allow_review=args.allow_license_review,
2166
+ prepared_data_dir=args.prepared_data_dir, prepared_data_zip=args.prepared_data_zip,
2167
+ stop_after=args.stop_after)
2168
 
2169
  if __name__ == "__main__":
2170
  main()
sources.lock.json CHANGED
@@ -1,17 +1,17 @@
1
  {
2
  "tool": "asterizer",
3
  "schema": 1,
4
- "created_utc": "2024-06-26T10:43:24Z",
5
  "note": "Pre-resolved HF commit SHAs + license decisions for a reproducible freeze. Review, commit, then build; the build pins load_dataset(revision=...) from here.",
6
  "datasets": {
7
  "HuggingFaceFW/fineweb-2": {
8
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9",
9
  "licenses": [
10
  "ODC-By-1.0"
11
  ]
12
  },
13
  "HuggingFaceFW/fineweb-edu": {
14
- "revision": "87f09149ef4734204d70ed1d046ddc9ca3f2bf9",
15
  "licenses": [
16
  "ODC-By-1.0"
17
  ]
@@ -117,15 +117,6 @@
117
  "license": "CC-BY-4.0",
118
  "revision": "8b813c3f62d37b2fa174d68c31e8b35ae2fe85e8"
119
  },
120
- {
121
- "bucket": "other_indic",
122
- "dataset": "ai4bharat/sangraha",
123
- "config": "verified/ory",
124
- "lang": "or",
125
- "script": "Orya",
126
- "license": "CC-BY-4.0",
127
- "revision": "8b813c3f62d37b2fa174d68c31e8b35ae2fe85e8"
128
- },
129
  {
130
  "bucket": "code",
131
  "dataset": "bigcode/starcoderdata",
@@ -268,7 +259,7 @@
268
  "lang": "en",
269
  "script": "Latn",
270
  "license": "ODC-By-1.0",
271
- "revision": "87f09149ef4734204d70ed1d046ddc9ca3f2bf9"
272
  },
273
  {
274
  "bucket": "european",
@@ -277,7 +268,7 @@
277
  "lang": "es",
278
  "script": "Latn",
279
  "license": "ODC-By-1.0",
280
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
281
  },
282
  {
283
  "bucket": "european",
@@ -286,7 +277,7 @@
286
  "lang": "fr",
287
  "script": "Latn",
288
  "license": "ODC-By-1.0",
289
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
290
  },
291
  {
292
  "bucket": "european",
@@ -295,7 +286,7 @@
295
  "lang": "de",
296
  "script": "Latn",
297
  "license": "ODC-By-1.0",
298
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
299
  },
300
  {
301
  "bucket": "european",
@@ -304,7 +295,7 @@
304
  "lang": "it",
305
  "script": "Latn",
306
  "license": "ODC-By-1.0",
307
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
308
  },
309
  {
310
  "bucket": "european",
@@ -313,7 +304,7 @@
313
  "lang": "pt",
314
  "script": "Latn",
315
  "license": "ODC-By-1.0",
316
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
317
  },
318
  {
319
  "bucket": "more_european",
@@ -322,7 +313,7 @@
322
  "lang": "el",
323
  "script": "Grek",
324
  "license": "ODC-By-1.0",
325
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
326
  },
327
  {
328
  "bucket": "more_european",
@@ -331,7 +322,7 @@
331
  "lang": "tr",
332
  "script": "Latn",
333
  "license": "ODC-By-1.0",
334
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
335
  },
336
  {
337
  "bucket": "more_european",
@@ -340,7 +331,7 @@
340
  "lang": "pl",
341
  "script": "Latn",
342
  "license": "ODC-By-1.0",
343
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
344
  },
345
  {
346
  "bucket": "more_european",
@@ -349,7 +340,7 @@
349
  "lang": "nl",
350
  "script": "Latn",
351
  "license": "ODC-By-1.0",
352
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
353
  },
354
  {
355
  "bucket": "more_european",
@@ -358,7 +349,7 @@
358
  "lang": "sv",
359
  "script": "Latn",
360
  "license": "ODC-By-1.0",
361
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
362
  },
363
  {
364
  "bucket": "more_european",
@@ -367,7 +358,7 @@
367
  "lang": "ka",
368
  "script": "Geor",
369
  "license": "ODC-By-1.0",
370
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
371
  },
372
  {
373
  "bucket": "more_european",
@@ -376,7 +367,7 @@
376
  "lang": "hy",
377
  "script": "Armn",
378
  "license": "ODC-By-1.0",
379
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
380
  },
381
  {
382
  "bucket": "sea_scripts",
@@ -385,7 +376,7 @@
385
  "lang": "th",
386
  "script": "Thai",
387
  "license": "ODC-By-1.0",
388
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
389
  },
390
  {
391
  "bucket": "sea_scripts",
@@ -394,7 +385,7 @@
394
  "lang": "vi",
395
  "script": "Latn",
396
  "license": "ODC-By-1.0",
397
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
398
  },
399
  {
400
  "bucket": "sea_scripts",
@@ -403,7 +394,7 @@
403
  "lang": "id",
404
  "script": "Latn",
405
  "license": "ODC-By-1.0",
406
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
407
  },
408
  {
409
  "bucket": "sea_scripts",
@@ -412,7 +403,7 @@
412
  "lang": "km",
413
  "script": "Khmr",
414
  "license": "ODC-By-1.0",
415
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
416
  },
417
  {
418
  "bucket": "sea_scripts",
@@ -421,7 +412,7 @@
421
  "lang": "my",
422
  "script": "Mymr",
423
  "license": "ODC-By-1.0",
424
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
425
  },
426
  {
427
  "bucket": "sea_scripts",
@@ -430,7 +421,7 @@
430
  "lang": "lo",
431
  "script": "Laoo",
432
  "license": "ODC-By-1.0",
433
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
434
  },
435
  {
436
  "bucket": "cjk",
@@ -439,7 +430,7 @@
439
  "lang": "zh",
440
  "script": "Hani",
441
  "license": "ODC-By-1.0",
442
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
443
  },
444
  {
445
  "bucket": "cjk",
@@ -448,7 +439,7 @@
448
  "lang": "ja",
449
  "script": "Jpan",
450
  "license": "ODC-By-1.0",
451
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
452
  },
453
  {
454
  "bucket": "cjk",
@@ -457,7 +448,7 @@
457
  "lang": "ko",
458
  "script": "Hang",
459
  "license": "ODC-By-1.0",
460
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
461
  },
462
  {
463
  "bucket": "semitic",
@@ -466,7 +457,7 @@
466
  "lang": "ar",
467
  "script": "Arab",
468
  "license": "ODC-By-1.0",
469
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
470
  },
471
  {
472
  "bucket": "semitic",
@@ -475,7 +466,7 @@
475
  "lang": "fa",
476
  "script": "Arab",
477
  "license": "ODC-By-1.0",
478
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
479
  },
480
  {
481
  "bucket": "semitic",
@@ -484,7 +475,7 @@
484
  "lang": "ur",
485
  "script": "Arab",
486
  "license": "ODC-By-1.0",
487
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
488
  },
489
  {
490
  "bucket": "semitic",
@@ -493,7 +484,7 @@
493
  "lang": "he",
494
  "script": "Hebr",
495
  "license": "ODC-By-1.0",
496
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
497
  },
498
  {
499
  "bucket": "cyrillic",
@@ -502,7 +493,7 @@
502
  "lang": "ru",
503
  "script": "Cyrl",
504
  "license": "ODC-By-1.0",
505
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
506
  },
507
  {
508
  "bucket": "cyrillic",
@@ -511,7 +502,7 @@
511
  "lang": "uk",
512
  "script": "Cyrl",
513
  "license": "ODC-By-1.0",
514
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
515
  },
516
  {
517
  "bucket": "african",
@@ -520,7 +511,7 @@
520
  "lang": "sw",
521
  "script": "Latn",
522
  "license": "ODC-By-1.0",
523
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
524
  },
525
  {
526
  "bucket": "african",
@@ -529,7 +520,7 @@
529
  "lang": "am",
530
  "script": "Ethi",
531
  "license": "ODC-By-1.0",
532
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
533
  },
534
  {
535
  "bucket": "african",
@@ -538,7 +529,7 @@
538
  "lang": "yo",
539
  "script": "Latn",
540
  "license": "ODC-By-1.0",
541
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
542
  },
543
  {
544
  "bucket": "african",
@@ -547,7 +538,7 @@
547
  "lang": "ha",
548
  "script": "Latn",
549
  "license": "ODC-By-1.0",
550
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
551
  },
552
  {
553
  "bucket": "coverage",
@@ -556,7 +547,7 @@
556
  "lang": "bo",
557
  "script": "Tibt",
558
  "license": "ODC-By-1.0",
559
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
560
  },
561
  {
562
  "bucket": "coverage",
@@ -565,7 +556,7 @@
565
  "lang": "si",
566
  "script": "Sinh",
567
  "license": "ODC-By-1.0",
568
- "revision": "af9c1333eb981300149d5ca60a8e9d659b276b9"
569
  },
570
  {
571
  "bucket": "math",
@@ -577,4 +568,4 @@
577
  "revision": "fde8ef8de2300f5e778f56261843dab89f230815"
578
  }
579
  ]
580
- }
 
1
  {
2
  "tool": "asterizer",
3
  "schema": 1,
4
+ "created_utc": "2026-06-28T03:15:48Z",
5
  "note": "Pre-resolved HF commit SHAs + license decisions for a reproducible freeze. Review, commit, then build; the build pins load_dataset(revision=...) from here.",
6
  "datasets": {
7
  "HuggingFaceFW/fineweb-2": {
8
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9",
9
  "licenses": [
10
  "ODC-By-1.0"
11
  ]
12
  },
13
  "HuggingFaceFW/fineweb-edu": {
14
+ "revision": "87f09149ef4734204d70ed1d046ddc9ca3f2b8f9",
15
  "licenses": [
16
  "ODC-By-1.0"
17
  ]
 
117
  "license": "CC-BY-4.0",
118
  "revision": "8b813c3f62d37b2fa174d68c31e8b35ae2fe85e8"
119
  },
 
 
 
 
 
 
 
 
 
120
  {
121
  "bucket": "code",
122
  "dataset": "bigcode/starcoderdata",
 
259
  "lang": "en",
260
  "script": "Latn",
261
  "license": "ODC-By-1.0",
262
+ "revision": "87f09149ef4734204d70ed1d046ddc9ca3f2b8f9"
263
  },
264
  {
265
  "bucket": "european",
 
268
  "lang": "es",
269
  "script": "Latn",
270
  "license": "ODC-By-1.0",
271
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
272
  },
273
  {
274
  "bucket": "european",
 
277
  "lang": "fr",
278
  "script": "Latn",
279
  "license": "ODC-By-1.0",
280
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
281
  },
282
  {
283
  "bucket": "european",
 
286
  "lang": "de",
287
  "script": "Latn",
288
  "license": "ODC-By-1.0",
289
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
290
  },
291
  {
292
  "bucket": "european",
 
295
  "lang": "it",
296
  "script": "Latn",
297
  "license": "ODC-By-1.0",
298
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
299
  },
300
  {
301
  "bucket": "european",
 
304
  "lang": "pt",
305
  "script": "Latn",
306
  "license": "ODC-By-1.0",
307
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
308
  },
309
  {
310
  "bucket": "more_european",
 
313
  "lang": "el",
314
  "script": "Grek",
315
  "license": "ODC-By-1.0",
316
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
317
  },
318
  {
319
  "bucket": "more_european",
 
322
  "lang": "tr",
323
  "script": "Latn",
324
  "license": "ODC-By-1.0",
325
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
326
  },
327
  {
328
  "bucket": "more_european",
 
331
  "lang": "pl",
332
  "script": "Latn",
333
  "license": "ODC-By-1.0",
334
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
335
  },
336
  {
337
  "bucket": "more_european",
 
340
  "lang": "nl",
341
  "script": "Latn",
342
  "license": "ODC-By-1.0",
343
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
344
  },
345
  {
346
  "bucket": "more_european",
 
349
  "lang": "sv",
350
  "script": "Latn",
351
  "license": "ODC-By-1.0",
352
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
353
  },
354
  {
355
  "bucket": "more_european",
 
358
  "lang": "ka",
359
  "script": "Geor",
360
  "license": "ODC-By-1.0",
361
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
362
  },
363
  {
364
  "bucket": "more_european",
 
367
  "lang": "hy",
368
  "script": "Armn",
369
  "license": "ODC-By-1.0",
370
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
371
  },
372
  {
373
  "bucket": "sea_scripts",
 
376
  "lang": "th",
377
  "script": "Thai",
378
  "license": "ODC-By-1.0",
379
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
380
  },
381
  {
382
  "bucket": "sea_scripts",
 
385
  "lang": "vi",
386
  "script": "Latn",
387
  "license": "ODC-By-1.0",
388
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
389
  },
390
  {
391
  "bucket": "sea_scripts",
 
394
  "lang": "id",
395
  "script": "Latn",
396
  "license": "ODC-By-1.0",
397
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
398
  },
399
  {
400
  "bucket": "sea_scripts",
 
403
  "lang": "km",
404
  "script": "Khmr",
405
  "license": "ODC-By-1.0",
406
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
407
  },
408
  {
409
  "bucket": "sea_scripts",
 
412
  "lang": "my",
413
  "script": "Mymr",
414
  "license": "ODC-By-1.0",
415
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
416
  },
417
  {
418
  "bucket": "sea_scripts",
 
421
  "lang": "lo",
422
  "script": "Laoo",
423
  "license": "ODC-By-1.0",
424
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
425
  },
426
  {
427
  "bucket": "cjk",
 
430
  "lang": "zh",
431
  "script": "Hani",
432
  "license": "ODC-By-1.0",
433
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
434
  },
435
  {
436
  "bucket": "cjk",
 
439
  "lang": "ja",
440
  "script": "Jpan",
441
  "license": "ODC-By-1.0",
442
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
443
  },
444
  {
445
  "bucket": "cjk",
 
448
  "lang": "ko",
449
  "script": "Hang",
450
  "license": "ODC-By-1.0",
451
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
452
  },
453
  {
454
  "bucket": "semitic",
 
457
  "lang": "ar",
458
  "script": "Arab",
459
  "license": "ODC-By-1.0",
460
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
461
  },
462
  {
463
  "bucket": "semitic",
 
466
  "lang": "fa",
467
  "script": "Arab",
468
  "license": "ODC-By-1.0",
469
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
470
  },
471
  {
472
  "bucket": "semitic",
 
475
  "lang": "ur",
476
  "script": "Arab",
477
  "license": "ODC-By-1.0",
478
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
479
  },
480
  {
481
  "bucket": "semitic",
 
484
  "lang": "he",
485
  "script": "Hebr",
486
  "license": "ODC-By-1.0",
487
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
488
  },
489
  {
490
  "bucket": "cyrillic",
 
493
  "lang": "ru",
494
  "script": "Cyrl",
495
  "license": "ODC-By-1.0",
496
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
497
  },
498
  {
499
  "bucket": "cyrillic",
 
502
  "lang": "uk",
503
  "script": "Cyrl",
504
  "license": "ODC-By-1.0",
505
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
506
  },
507
  {
508
  "bucket": "african",
 
511
  "lang": "sw",
512
  "script": "Latn",
513
  "license": "ODC-By-1.0",
514
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
515
  },
516
  {
517
  "bucket": "african",
 
520
  "lang": "am",
521
  "script": "Ethi",
522
  "license": "ODC-By-1.0",
523
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
524
  },
525
  {
526
  "bucket": "african",
 
529
  "lang": "yo",
530
  "script": "Latn",
531
  "license": "ODC-By-1.0",
532
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
533
  },
534
  {
535
  "bucket": "african",
 
538
  "lang": "ha",
539
  "script": "Latn",
540
  "license": "ODC-By-1.0",
541
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
542
  },
543
  {
544
  "bucket": "coverage",
 
547
  "lang": "bo",
548
  "script": "Tibt",
549
  "license": "ODC-By-1.0",
550
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
551
  },
552
  {
553
  "bucket": "coverage",
 
556
  "lang": "si",
557
  "script": "Sinh",
558
  "license": "ODC-By-1.0",
559
+ "revision": "af9c13333eb981300149d5ca60a8e9d659b276b9"
560
  },
561
  {
562
  "bucket": "math",
 
568
  "revision": "fde8ef8de2300f5e778f56261843dab89f230815"
569
  }
570
  ]
571
+ }