8.9 KiB
Генерация озвучки
Как из файлов игры получается мод, чем меряется качество и что крутить, чтобы стало лучше. Ни одна записка не прослушивается человеком: все решения принимаются по метрикам, а слушают уже в игре.
Конвейер
игра (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 |
Что крутить
- Лексикон (
ensvoice/lexicon.py) — самый дешёвый рычаг.PRE— замены в обычном тексте (сокращения, числа, опечатки оригинала),STRESS— ударения игровых имён: регулярное выражение основы и номер ударной гласной. Подсказки, что чинить: полеbadвwork/qc/*.json(слова с неверным ударением) иhypпротив текста у дублей с высоким WER. - Референс голоса (
voice.reference).python run.py refbenchозвучивает 16 абзацев каждым из 9 кандидатов, затемqc --jobs …иrefbench --reportдают таблицу. Референс с обрубленным концом «протекает»: дубли начинаются с английских слов, у таких кандидатов многоbadпо WER. - Сиды (
voice.seeds,extra_seeds): больше сидов — выше шанс на чистый дубль, время растёт линейно. - Паузы (
PAUSEвassemble.py) и порог нарезки (MAXCвsegments.py). - Отбор (
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 её названия и текста.