from __future__ import annotations import io import wave import numpy as np from piper import PiperVoice def load_voice(voice_path: str = "models/piper/cs_CZ-jirka-medium.onnx") -> PiperVoice: return PiperVoice.load(voice_path) def synthesize(voice: PiperVoice, text: str) -> tuple[np.ndarray, int]: """Synthesize `text` and return (mono float32 waveform in [-1,1], sample_rate).""" buf = io.BytesIO() with wave.open(buf, "wb") as wf: voice.synthesize_wav(text, wf) buf.seek(0) with wave.open(buf, "rb") as wf: sr = wf.getframerate() n_channels = wf.getnchannels() sampwidth = wf.getsampwidth() raw = wf.readframes(wf.getnframes()) if sampwidth != 2: raise RuntimeError(f"Unexpected Piper sample width: {sampwidth} bytes") pcm = np.frombuffer(raw, dtype=np.int16).astype(np.float32) / 32768.0 if n_channels > 1: pcm = pcm.reshape(-1, n_channels).mean(axis=1) return pcm, sr def synthesize_duration(voice: PiperVoice, text: str) -> tuple[float, np.ndarray, int]: """Convenience: returns (duration_seconds, waveform, sample_rate).""" wav, sr = synthesize(voice, text) return len(wav) / sr, wav, sr