Metadata-Version: 2.5
Name: sakhaspell
Version: 0.4.0
Summary: Проверка орфографии якутского языка: возвращает буквы ҕ ҥ ө һ ү и находит опечатки
Project-URL: Homepage, https://sakhaspell.michill.ru
Project-URL: Source, https://github.com/EgorovM/sakhaspell
Project-URL: Issues, https://github.com/EgorovM/sakhaspell/issues
Author-email: Michil Egorov <egorovmichil9@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: nlp,orthography,sakha,spellchecker,spelling,turkic,yakut
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Russian
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Provides-Extra: server
Requires-Dist: fastapi>=0.110; extra == 'server'
Requires-Dist: pydantic>=2; extra == 'server'
Requires-Dist: uvicorn>=0.27; extra == 'server'
Provides-Extra: tagger
Requires-Dist: torch>=2.0; extra == 'tagger'
Description-Content-Type: text/markdown

# sakhaspell — спелчекер якутского языка

**[sakhaspell.michill.ru](https://sakhaspell.michill.ru) — попробовать в браузере**

Проверка орфографии для якутского (саха тыла). Два слоя: словарный работает
офлайн на CPU, нейросетевой добавляет контекст.

Спелчекера для якутского до этого не было. Есть морфология —
[`apertium-sah`](https://github.com/apertium/apertium-sah) и
[`yakutmorph`](https://github.com/nicolascortegoso/yakutmorph), обе GPL-3 —
но hunspell-словаря, расширения для редакторов и нейрокорректора нет; ни
Яндекс.Спеллер, ни LanguageTool якутский не поддерживают. Разбор поля —
[`docs/research.md`](docs/research.md).

## Что умеет

```
$ python -m sakhaspell check "Ого уорэгэ кисиэхэ сана кыагы биэрэр"
1:0   Ого      →  Оҕо      [lexicon]   ещё: Ооо, Оо, Ошо
1:4   уорэгэ   →  үөрэҕэ   [lexicon]   ещё: үөрэнэ, үөрэрэ, үрэҕэ
1:11  кисиэхэ  →  киһиэхэ  [lexicon]   ещё: кимиэхэ, киниэхэ, биһиэхэ
1:19  сана     →  саҥа     [lexicon]   ещё: аана, саха, сара
1:24  кыагы    →  кыаҕы    [lexicon]   ещё: кыаны, кыргы, кыаһы
```

Три сценария:

- **восстановление ҕ ҥ ө һ ү** — текст набран с русской раскладки;
- **постобработка OCR и ASR** — машинные искажения (см. оговорку ниже);
- **проверка при наборе** — подчёркивание с подсказками.

## Результаты

SakhaSpellBench, **отложенный test**, 800 предложений на задачу. Метрики
пословные, как в [SAGE](https://github.com/ai-forever/sage).

| задача | словарь | + контекст | + тэггер | детекция | ложные |
|---|---|---|---|---|---|
| `clean` — чистый текст | — | — | — | — | **1.75%** |
| `denorm_full` — без раскладки | 93.16% | **94.40%** | **96.51%** | 98.88% | 1.92% |
| `denorm_mixed` — как пишут | 92.59% | **93.65%** | **95.87%** | 98.97% | 2.08% |
| `mixed` — опечатки поверх | 87.83% | **90.45%** | — | 95.76% | 1.81% |
| `typo` — опечатки | 76.26% | **81.32%** | — | 87.43% | 1.78% |
| `real` — ошибки из корпуса | 67.36% | **70.46%** | — | 65.35% | 2.35% |

Контекстная модель добавляет **5 пунктов на опечатках** и 3 на ошибках из
корпуса, тэггер — ещё 2 на деноминализации. Обе **не стоят ничего** в ложных
срабатываниях: они переставляют кандидатов и возвращают буквы, а решение
«это ошибка» принимает словарь.

Главная колонка — последняя. Ложные срабатывания на заведомо правильном тексте:
**1.75%** против 8.96% у apertium-sah (LREC 2022) и 4.89% помеченных токенов
у [bashspell](https://github.com/AigizK/bashspell), где ~46% помеченного ложно.
Именно эта метрика решает, оставит пользователь спелчекер включённым или нет.

Считается доля правильных слов, которые система **подчеркнула** зря, — из них
подсказку она предлагает для 1.33%, а остальные помечает без вариантов замены.
Подчёркивание пользователь видит в любом случае, поэтому метрика по нему; так же
меряют и apertium-sah с bashspell.

Скорость на CPU: проверка слова **0.0016 мс** (у bashspell 0.17–0.21 мс),
подсказки 51 мс медиана (у bashspell 115–132 мс), 392 слова/с на сплошном
тексте. Память 470 МБ. Тэггер — 10.8M параметров, посимвольный F1 0.9919.

### Оговорка про ASR

Задача постобработки ASR собрана из настоящих выходов распознавателя проекта
STT, и чекер на ней проваливается: F1 1.9%. Причина не в чекере — **эталонные
расшифровки грязнее, чем выход модели**. В 50.9% расхождений не является словом
именно эталон (против 13.6% у гипотезы), и 19.1% эталонов — деноминализованные
написания (против 0.7%).

Полный аудит 8 417 расшифровок: **46.0% содержат орфографическую ошибку**,
6.74% всех слов. Артефакты для починки — `data/asr_audit/ref_issues.jsonl` и
`top_bad_words.tsv`. Для проекта ASR это значит, что WER завышен: модель
штрафуется за слова, которые написала правильно.

## Устройство

```
L0  нормализация      NFC, невидимые символы, чужие кириллические буквы
                      (ѳ→ө, ң→ҥ, ғ→ҕ), латинские гомоглифы внутри слова
L1  словарь           301k словоформ из корпуса + фильтр теней искажения,
                      кандидаты через префиксное дерево со взвешенным
                      расстоянием, CPU, миллисекунды
L2  тэггер            посимвольная разметка: для каждой буквы решает, это «г»
                      или «ҕ». Неавторегрессивный, 10.8M параметров
LM  контекст          биграммы с откатом: выбирают между кандидатами равной
                      цены. 963k биграмм, 6.9 МБ, работают на CPU
R   правила           восемь правил из грамматики: гармония гласных, сочетания
                      гласных, позиционные ограничения, стечения согласных,
                      мягкий знак. Помогают ранжированию, доступны отдельно
```

```python
from sakhaspell import violations, is_wellformed, RULES

is_wellformed("оҕо")        # True
is_wellformed("сурэ")       # False — после «у» не бывает «э»
violations("уьу")           # [soft_sign@2:ь не после д/н]
list(RULES)                 # все восемь правил по именам
```

Три решения, которые определили результат:

**Первичный акцептор — словоформы, а не FST.** У apertium-sah точность 98.52%,
но наивное покрытие на газетах 91.04%: FST-акцептор подчёркивает 9% правильного
текста. Корпусный лексикон даёт 1.75%.

**Фильтр теней искажения.** Систематическая ошибка письма без раскладки
проникает в лексикон: `сана` (378 вхождений), `киси` (21), `ого` (17) попали в
словарь, потому что так пишут во всех источниках сразу, и правило «подтверждено
двумя источниками» против этого бессильно. Отдельная проверка находит формы,
которые являются искажением гораздо более частой формы, и убирает их. Это дало
**+27.7 пункта F1** и обрушило долю «невидимых без контекста» ошибок
деноминализации с 41.2% до 1.5%.

**Контекст решает там, где частота ошибается.** Верный вариант исправления уже
находился среди кандидатов в 93.8% случаев на опечатках, но первым мы его
ставили только в 80.5% — тринадцать пунктов терялись в ранжировании. Классика:
`сйын` → кандидаты `ыйын` и `сайын`, побеждает более частотный, хотя `бу сайын`
встречается 951 раз, а `бу ыйын` — ни разу. Биграммная модель это чинит.

**Книжная грамматика добавляет десятые доли.** Восемь правил из грамматики —
гармония гласных, сочетания гласных, позиционные ограничения, стечения
согласных, мягкий знак — проверены на 300 тысячах словоформ и измерены порознь.
Вместе они ловят 14.6% ошибок, невидимых для словаря, но помечать по ним слова
не окупается: ложные срабатывания растут с 1.79% до 3.53%. Причина —
заимствования: правила исконной фонетики они нарушают законно, а отличить их
без словаря нельзя. В умолчаниях остались только бесплатные применения —
подсказка ранжированию и восстановление вне словаря. Замер целиком —
[E15 и E17](docs/experiments.md).

Заодно корпус опроверг часть формулировок. Правило «слово не оканчивается на
б, г, ҕ, д, һ, ч» верно только для ҕ: на «һ» оканчивается `тыһ` с 14 753
вхождениями, а на б/г/д/ч — заимствования вроде `психолог` и `куб`.

**L2 — разметка, а не переписывание.** Задача восстановления ҕҥөһү посимвольная
по своей природе, и разметка ей точно соответствует: модель физически не может
изменить символ, которому предсказала «оставить». Seq2seq переписывает текст
целиком и правит то, о чём не просили.

Подробности с цифрами — [`docs/experiments.md`](docs/experiments.md).

## Установка

```bash
pip install sakhaspell
```

Словарь на 300 304 словоформы встроен в пакет — скачивать и настраивать нечего.
У словарного слоя нет зависимостей вообще, только стандартная библиотека.

```bash
sakhaspell check "Ого уорэгэ кисиэхэ"       # показать ошибки
sakhaspell fix --file статья.txt --in-place # исправить на месте
sakhaspell repl                             # интерактивно
cat текст.txt | sakhaspell fix > исправлено.txt
```

Свои имена и термины, чтобы их больше не подчёркивало:

```bash
sakhaspell dict add Ньургуйаана Хаҥалас
sakhaspell dict add "сахалыы -> саха тылынан"   # всегда исправлять
sakhaspell dict list
```

Из Python:

```python
from sakhaspell import SpellChecker, Lexicon

checker = SpellChecker(Lexicon.load())
checker.correct("Ого уорэгэ кисиэхэ")       # 'Оҕо үөрэҕэ киһиэхэ'

for issue in checker.check("Мин огом онгор"):
    print(issue.token.start, issue.token.text, issue.best)
```

Позиции правок в исходном тексте — для подсветки в редакторе:

```python
from sakhaspell import Pipeline

p = Pipeline()
for c in p.corrections("Ого уорэгэ кисиэхэ"):
    print(c.start, c.end, c.before, "→", c.after, c.alternatives)
```

### Контекстная модель

Даёт +3.5 пункта F1 на восстановлении ҕҥөһү. Чекпоинт в пакет не входит:
обучается за 33 минуты на одной H200 через `scripts/train_tagger.py`.

```bash
pip install "sakhaspell[tagger]"
sakhaspell --tagger runs/tagger_v2 check "текст"
```

### HTTP-сервис

```bash
pip install "sakhaspell[server]"
uvicorn sakhaspell.server:app --port 8080
```

`POST /check` — проверка текста с позициями, `POST /spell` — быстрый вердикт по
словам для подсветки, `POST /suggest` — подсказки по требованию. Разнесено
намеренно: проверка стоит 0.0016 мс на слово, подсказки — 51 мс.


## Сборка с нуля

Данные и обучение — на поде H200 (`docs/experiments.md`). Корпус — 8.2 млн
предложений из 18 источников.

```bash
python scripts/prep_sentences.py  --corpus corpus_sah.jsonl --out data/sent --procs 90
python scripts/build_lexicon.py   --sent data/sent --out data/lexicon
python scripts/mine_errors.py     --sent data/sent --lexicon data/lexicon --out data/errors
python scripts/find_shadows.py    --lexicon data/lexicon --out data/lexicon
python scripts/build_bench.py     --lexicon data/lexicon --errors data/errors --out data/bench
python scripts/eval_bench.py      --lexicon data/lexicon --bench data/bench --split dev
python scripts/train_tagger.py    --sent data/sent --out runs/tagger_v2 --steps 30000
```

Готовый словарь лежит в `sakhaspell/data/` сжатым и подхватывается сам;
`Lexicon.load("свой/каталог")` берёт другой.

## Сборка и выкладка

| Что | Когда | Куда |
|---|---|---|
| `test.yml` | push и PR | pytest на 3.10–3.13, плюс macOS и Windows |
| `publish.yml` | релиз с тегом | PyPI через доверенную публикацию |
| `deploy.yml` | изменения в `docs/` | GitHub Pages и sakhaspell.michill.ru |

Тесты проверяют не только код: отдельный шаг грузит встроенный словарь и
прогоняет через него фиксированные пары, чтобы порча данных не проехала молча.

## Страница

`docs/` — статическая страница: [sakhaspell.michill.ru](https://sakhaspell.michill.ru),
зеркало на [GitHub Pages](https://egorovm.github.io/sakhaspell/). Словарь пережат во
фронт-кодированный формат и весит **864 КБ** на все 300 тысяч форм, поэтому
проверка идёт целиком в браузере и текст никуда не отправляется. Пересборка:

```bash
python scripts/build_web_assets.py --lexicon data/lexicon --out docs/data
```

## Что не работает

- **Постобработка ASR.** Не потому, что чекер плох: эталонные расшифровки
  оказались грязнее выхода модели (см. оговорку выше).
- **Постобработка OCR не проверена** — нет пар «выход против эталона». Модель
  ошибок OCR добыта (52 015 пар), но проверить не на чем.
- **Опечатки — 76% F1.** Ошибку видим в 86% случаев, но верный вариант ставим
  первым не всегда: ранжирование учитывает цену правки и частоту, но не
  контекст.
- **Пунктуация и регистр** не восстанавливаются.

## Лицензии

Код проекта свой. `apertium-sah` и `yakutmorph` — GPL-3, в рантайме не
используются: лексикон построен из корпуса, поэтому результат не заражается.
