Files
UmbraAudio/backup/author_voice_lab/scripts/verify_cliff.py
2026-10-18 19:01:04 -07:00

90 lines
3.3 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Verify the downward-pitch intelligibility cliff on a LONGER text, 2 seeds.
The 25-word probe suggested WER collapses below ~0 st. WER on 25 words is coarse
(4% per word) and the earlier narrator probe was much gentler, so this re-tests
with ~3x the text and repeats each point.
"""
from __future__ import annotations
import io
import json
import re
import sys
import time
from pathlib import Path
import librosa
import numpy as np
import soundfile as sf
sys.path.insert(0, "/Volumes/1TB_APL_Storage/UmbraAudio")
from umbraaudio.generation.audio_effects import EFFECT_FN # noqa: E402
import httpx # noqa: E402
import whisper # noqa: E402
PROJECT = Path("/Volumes/1TB_APL_Storage/UmbraAudio")
REF = PROJECT / "DealHunterShip_4.0e_whole-to-audio/.umbra/voices/author_ref.wav"
OUT = Path("/private/var/folders/y8/cbvhds297918lrxt2wd3btsr0000gn/T/opencode/author_lab")
(OUT / "wav_cliff").mkdir(exist_ok=True)
LONG = (
"He set the lantern on the stone ledge and listened. The cavern breathed around "
"them, slow and patient, and somewhere far below the water was moving against rock. "
"MaryAnn kept one hand on the rail and waited for him to speak first, because she "
"had learned that what he said next would decide which of them went forward."
)
REF_WORDS = re.sub(r"[^a-z ]", "", LONG.lower()).split()
print(f"long text: {len(REF_WORDS)} words")
_pv = EFFECT_FN["pitch_shift_pv"]
wm = whisper.load_model("base", device="cpu")
def run(st, seed):
r = httpx.post("http://127.0.0.1:8881/v1/tts",
data={"text": LONG, "sample_rate": 24000, "temperature": 0.7,
"do_sample": True, "seed": seed},
files={"ref_audio": ("ref.wav", REF.read_bytes(), "audio/wav")},
timeout=900)
y, sr = sf.read(io.BytesIO(r.content), dtype="float32")
if y.ndim > 1:
y = y.mean(axis=1)
if st:
y = _pv(y, sr, semitones=float(st))
p = float(np.max(np.abs(y)))
if p > 1.0:
y = y / p
import difflib
hyp = re.sub(r"[^a-z ]", "",
wm.transcribe(y, language="en", fp16=False, temperature=0.0)["text"]
).lower().split()
n = sum(b.size for b in difflib.SequenceMatcher(None, REF_WORDS, hyp).get_matching_blocks())
f0, _, _ = librosa.pyin(y.astype(np.float32), fmin=60.0, fmax=400.0, sr=sr)
v = f0[~np.isnan(f0)]
sf.write(OUT / "wav_cliff" / f"cliff{st:+03d}_s{seed}.wav", y, sr, subtype="PCM_16")
return {"shift_st": st, "seed": seed, "wer": round((len(REF_WORDS) - n) / len(REF_WORDS), 3),
"f0": round(float(np.median(v)), 1), "hyp": " ".join(hyp)}
rows = []
for st in (0, -2, -4, -6, +2):
for seed in (7, 8):
t0 = time.time()
m = run(st, seed)
m["sec"] = round(time.time() - t0, 1)
rows.append(m)
print(f" shift {st:+3d} st seed {seed} WER {m['wer']:.3f} F0 {m['f0']:6.1f} Hz "
f"{m['sec']}s", flush=True)
(OUT / "cliff_verification.json").write_text(json.dumps(
{"text_words": len(REF_WORDS), "rows": rows}, indent=2) + "\n")
print("\n--- mean WER by shift ---")
for st in (0, -2, -4, -6, +2):
ws = [r["wer"] for r in rows if r["shift_st"] == st]
print(f" {st:+3d} st: mean WER {sum(ws)/len(ws):.3f} {ws}")