# Генерация озвучки Как из файлов игры получается мод, чем меряется качество и что крутить, чтобы стало лучше. Ни одна записка не прослушивается человеком: все решения принимаются по метрикам, а слушают уже в игре. ## Конвейер ``` игра (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/__s.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 её названия и текста.