Russian narration for the Enshrouded notes: mod 0.4.0, installer, generation pipeline

This commit is contained in:
2026-10-18 15:56:52 +03:00
commit 73dee2d786
557 changed files with 4051 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
# Генерация озвучки
Как из файлов игры получается мод, чем меряется качество и что крутить, чтобы стало лучше.
Ни одна записка не прослушивается человеком: все решения принимаются по метрикам, а слушают уже в игре.
## Конвейер
```
игра (enshrouded.kfc + .dat)
│ extract kfc-parser: дескрипторы озвучки, звука, локализации, журнала
│ EN-звук (frameless QOA 48 кГц) → work/en24/*.wav, RU-текст → текст для голоса
▼
work/notes.json ── refs ──► work/refs/*.wav референсы диктора (целые короткие записки и первые абзацы)
│ work/centroid.npy тембр диктора (средний x-вектор WavLM) — цель QC
│ work/en_pauses.json паузы диктора
│ segments lexicon.pre → абзацы ≤ 380 знаков → RUAccent (+) → lexicon.post → U+0301
▼
work/segs.json ── render ──► work/takes/<id>_<hash>_s<seed>.wav 3 сида на кусок
│ qc GigaAM-v3 (WER), meter2 (ударения), WavLM-SV (тембр), UTMOSv2 (MOS), темп
│ pick фильтр + счёт; без проходного дубля → render --weak (ещё 2 сида) → qc → pick
│ assemble обрезка, паузы диктора, 24→48 кГц soxr VHQ, громкость по оригиналу, лимитер, QOA
▼
work/qoa ── mod ──► mod/ru_voice_notes ── install.py ──► <игра>/mods + dbghelp.dll (EML)
```
`python run.py all` выполняет всё по порядку. Каждый шаг можно повторить отдельно: дубли и оценки
кэшируются по хэшу «референс + текст», поэтому после правки лексикона заново синтезируются и
проверяются только затронутые куски.
## Метрики
| Метрика | Чем | Что ловит |
|---|---|---|
| `wer`, `hyp` | GigaAM-v3 RNN-T по кускам ≤ 20 с | пропуски, оговорки, «протечку» референса (английский в начале) |
| `stress_ok / stress_n` | meter2 из модели ru-stress-cf | ударение не там, где поставил RUAccent или лексикон |
| `sim` | WavLM-SV, косинус к центроиду диктора | уход тембра |
| `mos` | UTMOSv2 | неестественность, артефакты |
| `cps` | знаков в секунду | затянутость или скороговорку, обрыв |
| `f0` (`qc --f0`) | разброс высоты тона, полутона | монотонность (медленно, на CPU) |
Отбор (`pick.py`): дубль проходит, если темп 7–19 знаков/с и WER ≤ 0,12 или ошибка не больше одного
слова (у кусков до 3 слов — CER ≤ 0,35: ASR коверкает одиночные имена). Среди прошедших —
максимум `6·stress + 0,5·MOS + 2·SIM − 3·WER`. Ударения весят больше всего: неверное ударение
слушатель слышит сразу.
Сравнивать версии — по `work/pick_report.json` и по одним и тем же целым запискам (`qc --jobs`).
## История: от 0.3 к 0.4 (4 итерации)
0.3.0 — вся записка одним дублем, референс `amethystHermit` (10 с), 3 сида, линейная интерполяция до 48 кГц.
| Итерация | Что изменено | Чем подтверждено |
|---|---|---|
| 1. Текст | лексикон имён (`Во́ргот`, `Э́мбервэйл`, `Ла́хлан`…), апострофы в именах (`Кра'Тау` → `Кра Тау`), сокращения (`ок.`, `мм`, `в т.ч.`), числа в падеже; ударения переносятся по словам — посимвольный перенос портил 4 % текстов («в го́дод», «))») | сверка текстов: 0 расхождений после правки |
| 2. Тембр | 8 кандидатов-референсов × 16 абзацев × 2 сида | победил `AlwaysTogether_Page_01__p1`: WER 3,7 % против 6,6 %, MOS 3,43 против 3,24, F0 5,30 против 4,52 |
| 3. Синтез | абзацы ≤ 380 знаков вместо целой записки, лучший дубль на каждый абзац, 2 доп. сида для слабых | без проходного дубля: 82 из 2660 (заголовки и имена, ложные) |
| 4. Сведение | паузы диктора (0,45 / 0,7 / 0,9 с вместо 0,3 / 0,7), soxr VHQ, мягкий лимитер | — |
Итог на 60 случайных записках, целиком:
| | 0.3.0 | 0.4.0 |
|---|---|---|
| WER | 3,2 % | 2,4 % |
| MOS | 3,27 | 3,45 (лучше в 41 из 60) |
| F0, разброс | 4,80 | 5,69 |
| SIM | 0,838 | 0,840 |
## Что крутить
1. **Лексикон** (`ensvoice/lexicon.py`) — самый дешёвый рычаг. `PRE` — замены в обычном тексте
(сокращения, числа, опечатки оригинала), `STRESS` — ударения игровых имён: регулярное выражение
основы и номер ударной гласной. Подсказки, что чинить: поле `bad` в `work/qc/*.json` (слова с
неверным ударением) и `hyp` против текста у дублей с высоким WER.
2. **Референс голоса** (`voice.reference`). `python run.py refbench` озвучивает 16 абзацев каждым из
9 кандидатов, затем `qc --jobs …` и `refbench --report` дают таблицу. Референс с обрубленным концом
«протекает»: дубли начинаются с английских слов, у таких кандидатов много `bad` по WER.
3. **Сиды** (`voice.seeds`, `extra_seeds`): больше сидов — выше шанс на чистый дубль, время растёт линейно.
4. **Паузы** (`PAUSE` в `assemble.py`) и порог нарезки (`MAXC` в `segments.py`).
5. **Отбор** (`passes` и `score` в `pick.py`).
Любую правку проверять числами: `pick_report.json` до и после и выборочные записки целиком.
## Время и ресурсы
Измерено на RTX 5080 16 ГБ (локальный бэкенд, батч 8):
| Шаг | Время |
|---|---|
| `extract` | 1–2 мин (CPU) |
| `refs` | 1–2 мин |
| `segments` | 2–5 мин (RUAccent на CPU, в основном загрузка моделей) |
| `render` | около 2 мин на 2500 знаков × сид → около 4 ч на сид, около 12 ч на 3 сида |
| `qc` | около 2,5 с на дубль → около 5 ч на 8000 дублей в один процесс; шарды `--shard i/n` параллельно |
| `assemble` | около 20 мин (QOA-кодер на чистом Python, CPU) |
Автор генерировал 0.4.0 на Tesla V100 через сервер `media-node` (qwentts.cpp, GGUF Q8_0): там синтез
примерно в 13 раз быстрее (RTF 0,05 против 0,6). Этот бэкенд (`tts.backend = "media_node"`) нужен, только если у вас есть такой сервер.
## Формат игры (для справки)
- `enshrouded.kfc`: заголовок KFC4 с 17 таблицами; ключи содержимого `(size, hash0, hash1, hash2)` →
смещение в `enshrouded_NNN.dat`.
- `VoiceoverResource.languages` — фиксированные слоты EN/DE/FR/ES/ZH, русского нет: мод подменяет EN.
- `SoundResource`: `FramelessQOA`, 48 кГц моно, `frameCount`, `duration` в нс, `streamedData.hash`.
- Frameless QOA: срезы QOA без заголовков, u64 little-endian на 20 сэмплов; LMS стартует с весов
`[0, 0, −8192, 16384]` и не сбрасывается.
- Локализация: `LocaTagCollectionResource`, записи по 24 байта (id, смещение строки от своего поля + 4, длина).
- `JournalRegistryResource` связывает озвучку записки с id её названия и текста.