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()