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
+74
View File
@@ -0,0 +1,74 @@
"""Step 6: pick the best take of every segment (work/pick.json, work/pick_report.json).
A take passes when it has no error, a pace of 7-19 chars/s and
- segments of up to 3 words (headings, names, signatures): CER <= 0.35 (ASR mangles lone names);
- longer ones: WER <= 0.12 or a single wrong word.
Among passing takes: score = 6*stress + 0.5*MOS + 2*SIM - 3*WER (stress first: a wrong stress is what
a listener hears). No passing take: the lowest WER, and the segment is listed as weak — `render --weak`
speaks it in voice.extra_seeds, then qc and pick again.
"""
import collections
import glob
import json
import re
import jiwer
from .config import load
def norm(t):
return " ".join(re.sub(r"[^\w\s]", " ", t.lower().replace("ё", "е")).split())
def passes(s, r):
if "error" in r or not 7 <= r["cps"] <= 19:
return False
if len(s["text"].split()) <= 3:
h = norm(r.get("hyp", ""))
return bool(h) and jiwer.cer(norm(s["text"]), h) <= 0.35
return r["wer"] <= 0.12 or r["wer"] * len(norm(s["text"]).split()) <= 1.01
def score(r):
return 6 * r["stress_ok"] / max(1, r["stress_n"]) + 0.5 * r.get("mos", 3) + 2 * r["sim"] - 3 * r["wer"]
def main():
cfg = load()
segs = json.load(open(cfg.w("segs.json"), encoding="utf-8"))
qc = {}
for f in sorted(glob.glob(str(cfg.w("qc", "x").parent / "*.json"))):
qc.update(json.load(open(f, encoding="utf-8")))
by = collections.defaultdict(dict)
for stem, r in qc.items():
take, seed = stem.rsplit("_s", 1)
by[take][int(seed)] = r
pick, weak, missing = {}, [], []
for s in segs:
takes = by.get(s["take"], {})
if not takes:
missing.append(s["id"]); continue
ok = {sd: r for sd, r in takes.items() if passes(s, r)}
if ok:
pick[s["id"]] = max(ok, key=lambda sd: score(ok[sd]))
else:
pick[s["id"]] = min(takes, key=lambda sd: takes[sd]["wer"])
weak.append(s["id"])
json.dump(pick, open(cfg.w("pick.json"), "w"))
take_of = {s["id"]: s["take"] for s in segs}
P = [by[take_of[i]][sd] for i, sd in pick.items()]
rep = dict(segments=len(segs), picked=len(pick), weak=weak, missing=missing,
wer=round(sum(r["wer"] for r in P) / max(1, len(P)), 4),
mos=round(sum(r.get("mos", 0) for r in P) / max(1, len(P)), 3),
sim=round(sum(r["sim"] for r in P) / max(1, len(P)), 3),
stress=round(sum(r["stress_ok"] for r in P) / max(1, sum(r["stress_n"] for r in P)), 4),
seeds=dict(collections.Counter(pick.values())))
json.dump(rep, open(cfg.w("pick_report.json"), "w"), indent=1)
print({k: (len(v) if isinstance(v, list) else v) for k, v in rep.items()})
if missing:
print(f"{len(missing)} segments have no scored take: run render and qc first")
if __name__ == "__main__":
main()