Metadata-Version: 2.4
Name: stress-uk
Version: 0.3.4
Summary: Розставляє наголоси (U+0301) в українському тексті - char-level BiLSTM + контекстний резолвер омографів
Author: stress-uk contributors
License: MIT
Project-URL: Homepage, https://github.com/MykhailoMS/stress-uk
Keywords: ukrainian,nlp,stress,accent,g2p,tts
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: torch>=2.0
Requires-Dist: huggingface_hub>=0.20
Requires-Dist: numpy
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Provides-Extra: embeddings
Requires-Dist: sentence-transformers>=2.2; extra == "embeddings"
Dynamic: license-file

# stress-uk

Розставляє наголоси в українському тексті символом **U+0301** (combining
acute accent), напр. `рука` → `рука́`. Гібрид: char-level нейромережа
(BiLSTM) для самого слова + rule-based шар для слів, де наголос залежить
від ЗНАЧЕННЯ в контексті (омографи, напр. за́мок-фортеця проти
замо́к-механізм).

## Встановлення

```bash
pip install stress-uk
```

Вага моделі (~9.5MB) НЕ входить у пакет — підвантажується автоматично з
HuggingFace Hub при першому використанні й кешується locally
(`~/.cache/huggingface/`), повторні запуски вже offline.

Опційно — few-shot резолвінг гетеронімів через ембединги контексту
(додаткові ~47 слів покриття поверх стем-тригерів, див. нижче):

```bash
pip install "stress-uk[embeddings]"
```

## Використання

```python
from stress_uk import stressify_text, stressify

print(stressify_text("Привіт, як справи?"))
# Приві́т, я́к спра́ви?

print(stressify("електрифікація"))
# електрифіка́ція
```

Для ансамблю кількох чекпоінтів або власної моделі:

```python
from stress_uk import Stressifier

s = Stressifier(checkpoint_paths=["шлях/до/мого_чекпоінта.pt"])
print(s.stressify_text("Текст."))
```

Розстановка наголосу в омографах залежить від слів навколо (±8 слів, без
перетину пунктуації, ближчі слова важать більше) — словник сенсів і
контекстних тригерів у `stress_uk/heteronyms.py` можна редагувати/
розширювати напряму, формат самоописний
(`HETERONYMS: dict[слово, tuple[Sense, ...]]`).

### Few-shot резолвінг через ембединги (опційно)

Стем-тригери — основний, дешевий механізм (мікросекунди). Якщо для
конкретного слова тригери НЕ дають однозначної відповіді (0 чи ≥2
матчі), і встановлено `stress-uk[embeddings]`, і для слова є приклади
речень у `stress_uk/embeddings.py` (`SENSE_EXAMPLES`) — пробується
few-shot класифікація через `sentence-transformers`: 2-3
приклади-речення на сенс кодуються в ембединги, усереднюються в
центроїд, нове входження класифікується за найближчим центроїдом
(cosine-відстань). Це НЕ заміна тригерів, а фолбек поверх них.

Покриває зараз **47 слів** (не весь словник - лише ті, для яких
знайшлися реальні речення в зовнішньому бенчмарку при розробці).
Помітно дорожче за тригери (~30мс/речення замість мікросекунд) - тому
й опційне, і фолбек, а не основний шлях. Без встановленого extra цей
шар просто вимикається (no-op), решта пакета працює як завжди.
Розширюється так само, як `HETERONYMS` - додай
`"слово": {"наголо́шена_форма": ["речення1", "речення2", ...]}` у
`SENSE_EXAMPLES`.

## Тренування власної моделі

```bash
pip install stress-uk
python -m stress_uk.train --epochs 2          # демо на маленькій вибірці, що йде з пакетом
```

Формат даних — JSONL, один приклад на рядок:
```json
{"word": "рука", "stress": [0, 0, 0, 1]}
```
де `word` — слово малими буквами без наголосу, `stress` — бінарна маска
тієї самої довжини (1 на позиції наголошеної букви). За замовчуванням
використовується невелика ілюстративна вибірка з пакета (НЕ повний
навчальний набір) — передай `--data-dir шлях/до/своїх/даних` зі своїми
`train.jsonl`/`val.jsonl`/`vocab.json`/`word_patterns.json` того самого
формату (`word_patterns.json` потрібен лише для лагідної оцінки під час
тренування: `{слово: {"best": [...], "all": [[...]]}}`).

Звичайний прогін ділить дані на train/val і зупиняється сам через early
stopping, чекпоінт зберігається в `./models/`. Для фінального
прод-чекпоінта (тренування на train+val разом, без held-out оцінки) —
`--full-data --epochs N`, де `N` — номер найкращої епохи звичайного
прогону.

## Розробка/публікація

Якщо клонував репозиторій (не просто `pip install`):

```bash
cd release
pip install -e .
```

Інструкції з публікації нової версії моделі на HuggingFace Hub і пакета
на PyPI — `scripts/PUBLISHING.md`.

## Вимоги

`torch` + `huggingface_hub` (CPU достатньо — інференс на слово займає
частки мілісекунди, тренування на CPU теж практично, бо модель невелика).
Опційно — `sentence-transformers` (`pip install "stress-uk[embeddings]"`)
для few-shot резолвінгу гетеронімів, див. розділ вище.

## Чого немає в цьому репозиторії

Щоб бути чесними з тими, хто захоче розібратися, як це зроблено, чи
повторити процес із власними даними:

- **Скрипти-краулери**, якими зібрано вихідні лексикографічні дані
  (зокрема дамп Вікісловника, uk.wiktionary.org, та ще кілька джерел) —
  не публікую. Вони специфічні під конкретні джерела й писались для
  одноразового внутрішнього використання, а не як перевикористовуваний
  інструмент.
- **Алгоритм об'єднання й узгодження кількох датасетів між собою** (як
  саме вирішується, який варіант наголосу "канонічний", коли різні
  джерела дають різну відповідь на те саме слово) — теж не публікую.
  Це найбільш ітеративно доопрацьована й специфічна частина роботи
  (там було знайдено й виправлено кілька нетривіальних багів — див.
  історію нижче), і вона лишається поза цим репозиторієм.

Публічно — повний код інференсу й тренування на вже підготовлених
даних (з прикладом невеликої вибірки того самого формату), словник
гетеронімів і сама натренована модель. Цього достатньо, щоб
користуватись інструментом і навіть перенавчити модель на власних
даних — просто без кроків "звідки взялись вихідні дані" і "як саме їх
звести в один узгоджений датасет".

## Історія розробки (коротко)

Базова модель — символьний (character-level) bidirectional LSTM:
embedding → 2-шаровий BiLSTM → лінійний шар, що для кожної букви слова
передбачає "ця буква наголошена чи ні" (multi-label, не softmax —
природно підтримує рідкі слова з кількома наголосами, зокрема дефісні
композити). Навчена на великому корпусі акцентованих українських
словоформ із кількох лексикографічних джерел, об'єднаних і узгоджених
між собою (конфліктні випадки — або очевидна друкарська помилка
джерела, яку виправляли голосуванням, або справжня граматична
омонімія, яку свідомо НЕ форсували до єдиної відповіді, а лишали
моделі вчити обидва варіанти з емпіричного розподілу).

Окремо — rule-based шар для омографів (слів, де те саме написання має
різний наголос залежно від значення): словник із кількох сотень
ручно/LLM-курованих записів, кожен запис — наголошені варіанти +
контекстні слова-тригери. Резолвер шукає тригери в ±8 словах навколо
цілі (з розривом контексту по будь-якій пунктуації) і зважує кандидатів
за відстанню (ближче слово — сильніший доказ), а не просто рахує
бінарний "матч/не матч".

Якість систематично перевірялась проти опублікованого незалежного
бенчмарку (Senyk et al., "Context-Aware Lexical Stress Prediction and
Phonemization for Ukrainian TTS Systems", UNLP 2025) — це дозволило
порівняти результат із кількома іншими опублікованими підходами на
тому самому тестовому наборі речень, а не лише на власних тестах.
Кілька раундів цільової діагностики (які саме слова й чому помиляються
на цьому бенчмарку, а не "сліпе" розширення словника) підняли
Macro-F1 на омографах із ~34% до ~49%, а також виявили і виправили
системний баг у підрахунку голосів між джерелами навчальних даних
(рідкісна граматична колізія могла переважити частотою формальних
словоформ значно частотніше в реальному вжитку слово). Фінальний
результат — найкращий за всіма п'ятьма метриками порівняння серед
усіх перевірених систем (включно з гібридними).
