File size: 3,765 Bytes
4d6a927 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | """Открытый тестовый набор: предложения из отложенных документов (в словарь не вошли)
+ искусственный «чатовый» шум по известным заменам (Леонтьев 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()
|