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

95 lines
4.1 KiB
Python

# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
"""Analyse the lab: variance vs separation, and verify a suspicious F0 reading."""
from __future__ import annotations
import json
from pathlib import Path
import librosa
import numpy as np
import soundfile as sf
LAB = Path("/private/var/folders/y8/cbvhds297918lrxt2wd3btsr0000gn/T/opencode/author_lab")
res = json.loads((LAB / "results.json").read_text())
rows = res["rows"]
REF_F0 = res["reference"]["f0_median"]
REF_ENV = res["reference"]["envelope_hz"]
print(f"reference: F0 {REF_F0} Hz envelope {REF_ENV} Hz\n")
def st(v, base):
return 12 * np.log2(v / base) if v and base else float("nan")
# ---------------------------------------------------------------- 1. variance
print("=== INTRA-VOICE VARIANCE (same voice, different seed/temperature) ===")
D = [r for r in rows if r["label"].startswith("D_")]
f0s = np.array([r["f0_median"] for r in D], dtype=float)
envs = np.array([r["envelope_hz"] for r in D], dtype=float)
print(f"n={len(D)} seeds 11-55 x temp 0.3/0.7/0.9")
print(f" F0 {f0s.min():.1f} - {f0s.max():.1f} Hz spread {f0s.max()-f0s.min():.1f} Hz "
f"= {st(f0s.max(), REF_F0)-st(f0s.min(), REF_F0):+.1f} st sd {f0s.std():.1f} Hz")
print(f" envelope {envs.min()} - {envs.max()} Hz spread "
f"{st(envs.max(), REF_ENV)-st(envs.min(), REF_ENV):+.1f} st sd {envs.std():.0f} Hz")
F0_SD_ST = f0s.std() / REF_F0 * 12 * np.log2(np.e) # semitone sd
ENV_SD_ST = envs.std() / REF_ENV * 12 * np.log2(np.e)
print(f" => 1-sd noise floor: F0 {F0_SD_ST:.2f} st, envelope {ENV_SD_ST:.2f} st")
print(f" => 2-sd (where a variant becomes reliably 'not just a re-roll'): "
f"F0 {2*F0_SD_ST:.2f} st")
# ---------------------------------------------------------------- 2. separation
print("\n=== AXIS B: did the neural instruct move anything on a REAL HUMAN reference? ===")
print(f"{'variant':20} {'d_F0_st':>9} {'d_env_st':>10} {'WER':>6}")
for r in rows:
if r["label"].startswith("B_") or r["label"].startswith("A00"):
print(f"{r['label']:20} {r.get('d_f0_st'):>9} {r.get('d_env_st'):>10} {r.get('wer'):>6}")
# ---------------------------------------------------------------- 3. verify F0 artifact
print("\n=== VERIFYING C_monster_grit_full (F0 69.7 Hz but WER 0.04) ===")
def true_f0(y, sr):
"""Autocorrelation F0 over 60-400 Hz - independent of pyin's tracking."""
y = y.astype(np.float32)
f0, _, _ = librosa.pyin(y, fmin=60.0, fmax=400.0, sr=sr, frame_length=1024)
v = f0[~np.isnan(f0)]
ac = librosa.autocorrelate(y, max_size=int(sr / 60))
lo, hi = int(sr / 400), int(sr / 60)
peak = lo + int(np.argmax(ac[lo:hi]))
return float(np.median(v)), sr / peak
for label in ("A00_baseline", "C_monster_grit_ring", "C_monster_grit_full",
"C_monster_pitch-10"):
p = LAB / "wav" / f"{label}.wav"
if not p.exists():
continue
y, sr = sf.read(str(p), dtype="float32")
if y.ndim > 1:
y = y.mean(axis=1)
pyin_f0, ac_f0 = true_f0(y, sr)
# strong spectral peaks
S = np.abs(librosa.stft(y, n_fft=4096, hop_length=1024))
f = librosa.fft_frequencies(sr=sr, n_fft=4096)
m = (f > 40) & (f < 500)
amp = S[m].mean(axis=1)
peaks = f[m][np.argsort(amp)[-4:]][::-1]
print(f" {label:24} pyin={pyin_f0:6.1f} autocorr={ac_f0:6.1f} Hz "
f"low-freq peaks={[round(float(x)) for x in peaks]}")
# ---------------------------------------------------------------- 4. cast headroom
print("\n=== CAST HEADROOM on the pitch axis ===")
A = [r for r in rows if r["label"].startswith("A_pitch") and r.get("f0_median")]
sep = np.array([abs(st(r["f0_median"], REF_F0)) for r in A])
print(f" reliable separation needs > 2-sd = {2*F0_SD_ST:.1f} st")
print(f" usable DSP variants found (|dF0| > 2sd): "
f"{sorted(round(float(x),1) for x in sep if x > 2*F0_SD_ST)}")
usable = [r for r in A if abs(st(r["f0_median"], REF_F0)) > 2 * F0_SD_ST]
lo = min(st(r["f0_median"], REF_F0) for r in usable)
hi = max(st(r["f0_median"], REF_F0) for r in usable)
step = 2 * F0_SD_ST
print(f" span {lo:+.1f}..{hi:+.1f} st / {step:.1f} st per voice => "
f"~{int((hi-lo)//step)+1} reliably distinct voices on ONE axis")