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
+64
View File
@@ -0,0 +1,64 @@
"""Step 8: build the EML mod into the repository (mod/<id>: the result that is committed).
usage: mod then `python install.py` copies it into the game and dry-runs it
The mod replaces each note's EN sound (the Russian slot does not exist in VoiceoverResource: its fixed
languages are EN/DE/FR/ES/ZH): SoundResource.streamedData points to the new QOA content, frameCount and
duration follow it, VoiceoverResource.durations[EN] too (10 ms units). The game files are not touched:
EML (dbghelp.dll proxy) patches enshrouded.kfc at launch and keeps enshrouded.kfc.bak (emm restore).
"""
import json
import pathlib
import shutil
from . import __version__
from .config import ROOT, load
LUA = '''local lines = require("lines")
local ok, fail = 0, 0
for _, l in ipairs(lines) do
local done, err = pcall(function()
local snd = game.assets.get_resource(l.sound, "keen::SoundResource", 0)
local d = snd.data
local content = game.assets.create_content(io.read("data/" .. l.sound .. ".qoa"))
d.streamedData.hash = content
d.frameCount = l.frames
d.framesPerSecond = 48000
d.duration.value = math.floor(l.seconds * 1e9)
local vo = game.assets.get_resource(l.vo, "keen::VoiceoverResource", 0)
vo.data.durations[1] = math.floor(l.seconds * 100)
end)
if done then ok = ok + 1 else fail = fail + 1; print("[ru_voice] FAIL " .. l.name .. ": " .. tostring(err)) end
end
print(("[ru_voice] replaced %d notes, %d failed"):format(ok, fail))
'''
def main():
cfg = load()
notes = json.load(open(cfg.w("notes.json"), encoding="utf-8"))
metas = [json.load(open(cfg.w("qoa", f"{n['k']}.json"))) for n in notes if cfg.w("qoa", f"{n['k']}.json").exists()]
if len(metas) < len(notes):
print(f"WARNING: {len(metas)} of {len(notes)} notes are assembled; the others stay English")
mod = pathlib.Path(cfg.get("mod", "dir") or ROOT / "mod" / cfg.get("mod", "id", "ru_voice_notes"))
if mod.exists():
shutil.rmtree(mod)
(mod / "src").mkdir(parents=True)
(mod / "data").mkdir()
for m in metas:
shutil.copy(cfg.w("qoa", f"{m['sound']}.qoa"), mod / "data" / f"{m['sound']}.qoa")
lines = ["-- generated by ensvoice: one entry per voiced note", "return {"]
lines += [f' {{ sound = "{m["sound"]}", vo = "{m["vo"]}", frames = {m["frames"]}, seconds = {m["seconds"]:.6f}, '
f'name = "{m["name"]}" }},' for m in metas]
(mod / "src" / "lines.lua").write_text("\n".join(lines + ["}"]) + "\n", encoding="utf-8")
(mod / "src" / "mod.lua").write_text(LUA, encoding="utf-8")
json.dump(dict(id=cfg.get("mod", "id", "ru_voice_notes"), name=cfg.get("mod", "name", "RU voice: notes"),
version=cfg.get("mod", "version", __version__), capabilities=["patch", "export"],
description="Russian narration for the voiced notes (Qwen3-TTS ru-stress, narrator clone, "
"automatic QC). Replaces the English voiceover slot.",
authors=cfg.get("mod", "authors", [])), open(mod / "mod.json", "w"), indent=2)
mb = sum(f.stat().st_size for f in (mod / "data").iterdir()) / 2 ** 20
print("mod", mod, len(metas), "notes", round(mb, 1), "MB; install: python install.py")
if __name__ == "__main__":
main()