Download scripts/build_lexicon.py from Sulus-ai/sakha-normalizer: direct link, hf CLI and curl.
- Browser
- Download file 5.3 kB
-
https://huggingface.co/Sulus-ai/sakha-normalizer/resolve/main/scripts/build_lexicon.py
- Command line
-
hf download hf://Sulus-ai/sakha-normalizer/scripts/build_lexicon.py
-
curl -L -o build_lexicon.py https://huggingface.co/Sulus-ai/sakha-normalizer/resolve/main/scripts/build_lexicon.py
5.3 kB
| """Собирает словари словоформ для нормализатора из открытых корпусов. | |
| Источники (только открытые лицензии): | |
| - ailabykt/sakha-corpus-mono (CC BY 4.0) -> якутский словарь | |
| - Википедия на якутском, дамп 2026-09-01 (CC BY-SA 4.0) -> якутский словарь | |
| - ailabykt/sakha-russian-parallel, колонка ru (CC BY 4.0) -> русский словарь | |
| 0,3% документов откладываются (по хешу) и в словарь не идут: из них собирается | |
| открытый тестовый набор (scripts/make_testset.py). | |
| Запуск (нужен pyarrow только для сборки, сам нормализатор работает без него): | |
| python scripts/build_lexicon.py --root "../" # корень папки sakha language | |
| """ | |
| import argparse, collections, glob, gzip, hashlib, json, os, re, sys | |
| VARIANTS = str.maketrans({'ғ': 'ҕ', 'ң': 'ҥ', 'ӈ': 'ҥ', 'ɵ': 'ө', 'ӧ': 'ө', 'ӱ': 'ү', 'ё': 'ё'}) | |
| TOKEN = re.compile(r'[^\W\d_]+') | |
| CYR = re.compile(r'^[а-яёҕҥөһү]+$') | |
| SAH_LETTERS = set('ҕҥөһү') | |
| def held_out(key: str) -> bool: | |
| return int(hashlib.md5(key.encode('utf-8')).hexdigest(), 16) % 1000 < 3 | |
| def tokens(text): | |
| for t in TOKEN.findall(text.lower().translate(VARIANTS)): | |
| if CYR.match(t): | |
| yield t | |
| def sakha_lines(text, min_share=0.025): | |
| """Строки, набранные с якутской раскладкой (доля ҕҥөһү среди букв >= 2,5%). | |
| В вебе и OCR есть якутский текст без якутских букв («кун» вместо «күн»): | |
| если считать его, «грязные» формы попадут в словарь как правильные. | |
| Русские строки тоже отсекаются: для них отдельный словарь. | |
| """ | |
| for line in text.split('\n'): | |
| low = line.lower() | |
| letters = sum(c.isalpha() for c in low) | |
| if not letters: | |
| continue | |
| if sum(c in 'ҕҥөһү' for c in low) / letters >= min_share: | |
| yield line | |
| def sah_tokens(text): | |
| for line in sakha_lines(text): | |
| yield from tokens(line) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument('--root', required=True, help='папка sakha language') | |
| ap.add_argument('--out', default=os.path.join(os.path.dirname(__file__), '..', 'sakha_normalizer', 'data')) | |
| ap.add_argument('--holdout', default=os.path.join(os.path.dirname(__file__), '..', 'eval', 'holdout_docs.jsonl')) | |
| ap.add_argument('--min-freq', type=int, default=5) | |
| a = ap.parse_args() | |
| import pyarrow.parquet as pq | |
| sah = collections.Counter() | |
| ru = collections.Counter() | |
| stats = collections.Counter() | |
| hold = open(a.holdout, 'w', encoding='utf-8') | |
| for path in sorted(glob.glob(os.path.join(a.root, 'raw', 'ailabykt_mono', 'data', '*.parquet'))): | |
| pf = pq.ParquetFile(path) | |
| for batch in pf.iter_batches(batch_size=2000, columns=['text', 'source']): | |
| for text, src in zip(batch.column('text').to_pylist(), batch.column('source').to_pylist()): | |
| if not text: | |
| continue | |
| key = 'ailabykt:' + hashlib.md5(text[:2000].encode('utf-8')).hexdigest() | |
| if held_out(key): | |
| hold.write(json.dumps({'source': 'ailabykt/sakha-corpus-mono:' + src, 'text': text}, ensure_ascii=False) + '\n') | |
| stats['held_docs'] += 1 | |
| continue | |
| sah.update(sah_tokens(text)) | |
| stats['docs_ailabykt'] += 1 | |
| print(path, len(sah), file=sys.stderr) | |
| for line in open(os.path.join(a.root, 'data', 'sahwiki.jsonl'), encoding='utf-8'): | |
| r = json.loads(line) | |
| if held_out('wiki:' + r['title']): | |
| hold.write(json.dumps({'source': 'sah.wikipedia:' + r['title'], 'text': r['text']}, ensure_ascii=False) + '\n') | |
| stats['held_docs'] += 1 | |
| continue | |
| sah.update(sah_tokens(r['text'])) | |
| stats['docs_wiki'] += 1 | |
| hold.close() | |
| pf = pq.ParquetFile(os.path.join(a.root, 'raw', 'ailabykt_parallel', 'sakha-russian.parquet')) | |
| for batch in pf.iter_batches(batch_size=5000, columns=['ru']): | |
| for text in batch.column('ru').to_pylist(): | |
| if text: | |
| ru.update(t for t in tokens(text) if not (set(t) & SAH_LETTERS)) | |
| os.makedirs(a.out, exist_ok=True) | |
| for name, cnt, mf in (('sah_lexicon.tsv.gz', sah, a.min_freq), ('ru_lexicon.tsv.gz', ru, 2)): | |
| kept = [(w, n) for w, n in cnt.items() if n >= mf] | |
| kept.sort(key=lambda x: (-x[1], x[0])) | |
| with gzip.open(os.path.join(a.out, name), 'wt', encoding='utf-8', compresslevel=9) as f: | |
| for w, n in kept: | |
| f.write(f'{w}\t{n}\n') | |
| stats[name + ':forms'] = len(kept) | |
| stats[name + ':tokens'] = sum(n for _, n in kept) | |
| print(json.dumps(stats, ensure_ascii=False, indent=1)) | |
| json.dump(stats, open(os.path.join(a.out, 'lexicon_stats.json'), 'w'), ensure_ascii=False, indent=1) | |
| if __name__ == '__main__': | |
| main() | |