| """Voice anchor lab (RTX 4090): real timbre variety, measured not hoped. |
| |
| VoxCPM2 ignores textual style descriptions (verified: it reads them aloud). |
| Without a reference it samples a RANDOM voice per run — so: generate many |
| candidates of the SAME neutral line, measure fundamental frequency (f0) and |
| loudness, pick three genuinely distinct timbres (low/mid/high pitch), peak- |
| normalize, and emit anchors + a report. Human ears make the final call. |
| """ |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
| import soundfile as sf |
|
|
| OUT = Path(__file__).resolve().parent / "voice_candidates" |
| ANCHOR_TEXT = "Okay, okay. It was me. Take me in. But write this down properly, detective." |
| N_CANDIDATES = 14 |
|
|
|
|
| def f0_median(wav: np.ndarray, sr: int) -> float: |
| import librosa |
| f0 = librosa.yin(wav.astype(np.float32), fmin=60, fmax=400, sr=sr) |
| voiced = f0[(f0 > 65) & (f0 < 390)] |
| return float(np.median(voiced)) if voiced.size else 0.0 |
|
|
|
|
| def main(): |
| from voxcpm import VoxCPM |
|
|
| OUT.mkdir(exist_ok=True) |
| tts = VoxCPM.from_pretrained("openbmb/VoxCPM2") |
| sr = tts.tts_model.sample_rate |
| print(f"sample rate: {sr}") |
|
|
| rows = [] |
| for i in range(N_CANDIDATES): |
| wav = np.asarray(tts.generate(text=ANCHOR_TEXT), dtype=np.float32) |
| dur = len(wav) / sr |
| if not 2.0 < dur < 12.0: |
| print(f"cand {i:02d}: DEGENERATE ({dur:.1f}s) — skipped") |
| continue |
| peak = float(np.abs(wav).max()) or 1.0 |
| wav = wav * (0.70 / peak) |
| pitch = f0_median(wav, sr) |
| path = OUT / f"cand_{i:02d}.wav" |
| sf.write(path, wav, sr) |
| rows.append((i, pitch, dur, path)) |
| print(f"cand {i:02d}: f0={pitch:5.1f} Hz dur={dur:4.1f}s") |
|
|
| rows.sort(key=lambda r: r[1]) |
| picks = {"low": rows[0], "mid": rows[len(rows) // 2], "high": rows[-1]} |
| print("\n=== SELECTED ANCHORS ===") |
| for name, (i, pitch, dur, path) in picks.items(): |
| dest = OUT / f"anchor_{name}.wav" |
| dest.write_bytes(path.read_bytes()) |
| print(f"{name:4s}: cand_{i:02d} f0={pitch:.0f} Hz dur={dur:.1f}s -> {dest.name}") |
| print(f"\nspread: {picks['high'][1] - picks['low'][1]:.0f} Hz between low and high") |
| print(f"listen: mpv {OUT}/anchor_low.wav etc. — Fernando decides.") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|