Files

8.9 KiB
Raw Permalink Blame History

Генерация озвучки

Как из файлов игры получается мод, чем меряется качество и что крутить, чтобы стало лучше. Ни одна записка не прослушивается человеком: все решения принимаются по метрикам, а слушают уже в игре.

Конвейер

игра (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 её названия и текста.