Download scripts/make_testset.py from Sulus-ai/sakha-normalizer: direct link, hf CLI and curl.
- Browser
- Download file 3.77 kB
-
https://huggingface.co/Sulus-ai/sakha-normalizer/resolve/main/scripts/make_testset.py
- Command line
-
hf download hf://Sulus-ai/sakha-normalizer/scripts/make_testset.py
-
curl -L -o make_testset.py https://huggingface.co/Sulus-ai/sakha-normalizer/resolve/main/scripts/make_testset.py
3.77 kB
| """Открытый тестовый набор: предложения из отложенных документов (в словарь не вошли) | |
| + искусственный «чатовый» шум по известным заменам (Леонтьев 2014; Эверстова и др. 2026). | |
| Выход: eval/testset.jsonl — поля id, split (dev/test), profile, gold, noisy, source. | |
| Лицензия текстов — как у источника (CC BY 4.0 / CC BY-SA 4.0), см. SOURCES.md. | |
| """ | |
| import hashlib, json, os, random, re | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| SRC = os.path.join(HERE, '..', 'eval', 'holdout_docs.jsonl') | |
| OUT = os.path.join(HERE, '..', 'eval', 'testset.jsonl') | |
| def mapper(table): | |
| def f(s, rng): | |
| return ''.join(_pick(table, ch, rng) for ch in s) | |
| return f | |
| def _pick(table, ch, rng): | |
| low = ch.lower() | |
| if low not in table: | |
| return ch | |
| v = table[low] | |
| if isinstance(v, (list, tuple)): | |
| v = rng.choice(v) | |
| return v.upper() if ch.isupper() and len(v) == 1 and v.isalpha() else v | |
| PROFILES = { | |
| # как в генераторе make_noisy.py: якутские буквы → русские, һ → латинская h | |
| 'ru_fold': mapper({'ҕ': 'г', 'ҥ': 'н', 'ө': 'о', 'ү': 'у', 'һ': 'h'}), | |
| # Леонтьев 2014: ө→е, ү→у, һ→ь (+ ҕ→г, ҥ→н) | |
| 'leontiev': mapper({'ҕ': 'г', 'ҥ': 'н', 'ө': 'е', 'ү': 'у', 'һ': 'ь'}), | |
| # цифры и латиница | |
| 'digits': mapper({'ҕ': '5', 'ҥ': 'н', 'ө': '8', 'ү': 'y', 'һ': 'h'}), | |
| # Эверстова и др. 2026: вперемешку ө→о/8, ҕ→г/х/5, һ→ь/с/h | |
| 'mixed': mapper({'ҕ': ['г', 'х', '5'], 'ҥ': ['н', 'нг'], 'ө': ['о', '8'], 'ү': ['у', 'y'], 'һ': ['ь', 'с', 'h']}), | |
| # якутская раскладка есть, но без ҕ и ҥ | |
| 'partial': mapper({'ҕ': 'г', 'ҥ': 'н'}), | |
| # без изменений: проверка, что правильный текст не портится | |
| 'clean': lambda s, rng: s, | |
| } | |
| SENT = re.compile(r'(?<=[.!?…])\s+') | |
| def main(): | |
| rng = random.Random(2026) | |
| sents = [] | |
| for line in open(SRC, encoding='utf-8'): | |
| d = json.loads(line) | |
| if ':natlib' in d['source']: | |
| continue # OCR, много ошибок распознавания | |
| split = 'dev' if int(hashlib.md5(d['source'].encode() + d['text'][:200].encode()).hexdigest(), 16) % 2 == 0 else 'test' | |
| for s in SENT.split(d['text']): | |
| s = ' '.join(s.split()) | |
| if not 40 <= len(s) <= 220: | |
| continue | |
| letters = [c for c in s.lower() if c.isalpha()] | |
| if not letters or sum(c in 'ҕҥөһү' for c in letters) / len(letters) < 0.03: | |
| continue # нет якутских букв: русский текст или OCR без них | |
| if re.search(r'[A-Za-z]', s): | |
| continue | |
| sents.append((split, d['source'], s)) | |
| rng.shuffle(sents) | |
| seen = set(); rows = [] | |
| profiles = list(PROFILES) | |
| for split, src, s in sents: | |
| if s in seen: | |
| continue | |
| seen.add(s) | |
| prof = profiles[len(rows) % len(profiles)] | |
| rows.append({'id': len(rows) + 1, 'split': split, 'profile': prof, 'gold': s, | |
| 'noisy': PROFILES[prof](s, rng), 'source': src}) | |
| if len(rows) >= 3000: | |
| break | |
| with open(OUT, 'w', encoding='utf-8') as f: | |
| for r in rows: | |
| f.write(json.dumps(r, ensure_ascii=False) + '\n') | |
| print(len(rows), 'предложений →', OUT, {p: sum(r['split'] == p for r in rows) for p in ('dev', 'test')}) | |
| if __name__ == '__main__': | |
| main() | |