Files
enshrouded-ru-voice/docs/GENERATION.md
T

109 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Генерация озвучки
Как из файлов игры получается мод, чем меряется качество и что крутить, чтобы стало лучше.
Ни одна записка не прослушивается человеком: все решения принимаются по метрикам, а слушают уже в игре.
## Конвейер
```
игра (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 её названия и текста.