90 lines
3.3 KiB
Python
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}")
|