| import subprocess |
| import os |
| import numpy as np |
| import librosa |
| import onnxruntime as ort |
| import openwakeword |
|
|
|
|
| class PiperSynthesizer: |
| def __init__(self, piper_path=None, model_path=None, config_path=None): |
| self.piper_path = piper_path or "piper" |
| self.model_path = model_path |
| self.config_path = config_path |
|
|
| def synthesize(self, text, output_wav_path, |
| length_scale=1.0, noise_scale=0.0, noise_w=0.0): |
| cmd = [ |
| self.piper_path, |
| "--model", self.model_path, |
| "--config", self.config_path, |
| "--output_file", output_wav_path, |
| "--length_scale", str(length_scale), |
| "--noise_scale", str(noise_scale), |
| "--noise_w", str(noise_w), |
| ] |
| try: |
| result = subprocess.run( |
| cmd, input=text, capture_output=True, text=True, timeout=15 |
| ) |
| if result.returncode == 0 and os.path.exists(output_wav_path): |
| return True |
| except subprocess.TimeoutExpired: |
| pass |
| return False |
|
|
|
|
| def load_embedding_model(): |
| """ |
| Load the speech embedding ONNX model bundled with openWakeWord. |
| Returns (onnxruntime.InferenceSession, input_name). |
| """ |
| model_path = os.path.join( |
| os.path.dirname(openwakeword.__file__), |
| "resources", "models", "embedding_model.onnx" |
| ) |
| if not os.path.exists(model_path): |
| openwakeword.utils.download_models() |
| if not os.path.exists(model_path): |
| raise FileNotFoundError(f"Embedding model not found at {model_path}") |
|
|
| session = ort.InferenceSession(model_path) |
|
|
| |
| inp = session.get_inputs()[0] |
| print(f"Embedding model input β name: '{inp.name}', shape: {inp.shape}, type: {inp.type}") |
|
|
| return session |
|
|
|
|
| def _compute_mel_spectrogram(audio, sr=16000, n_mels=32, n_fft=512, |
| hop_length=160, win_length=400): |
| """ |
| Compute a log mel spectrogram matching openWakeWord's preprocessing: |
| - 16 kHz mono audio |
| - 32 mel bins |
| - 25 ms windows, 10 ms hop |
| Returns array of shape (time_frames, 32). |
| """ |
| mel = librosa.feature.melspectrogram( |
| y=audio, |
| sr=sr, |
| n_mels=n_mels, |
| n_fft=n_fft, |
| hop_length=hop_length, |
| win_length=win_length, |
| fmin=60.0, |
| fmax=3800.0, |
| ) |
| log_mel = librosa.power_to_db(mel, ref=1.0).T |
| return log_mel.astype(np.float32) |
|
|
|
|
| def extract_embedding(session, wav_path, target_sr=16000, |
| frame_len=76, hop_frames=8): |
| """ |
| Extract a speech embedding from a WAV file. |
| |
| The openWakeWord embedding model expects input of shape: |
| (batch, frame_len, n_mels, 1) β e.g. (N, 76, 32, 1) |
| |
| We slide a window over the mel spectrogram and average the resulting |
| per-frame embeddings into a single vector. |
| |
| Parameters: |
| session : onnxruntime InferenceSession from load_embedding_model() |
| wav_path : path to audio file |
| target_sr : sample rate (must be 16000) |
| frame_len : number of mel frames per window (76 β 0.96 s) |
| hop_frames : step between windows |
| |
| Returns: |
| numpy array of shape (embedding_dim,), or None on error. |
| """ |
| try: |
| audio, _ = librosa.load(wav_path, sr=target_sr, mono=True) |
| log_mel = _compute_mel_spectrogram(audio, sr=target_sr) |
|
|
| T, n_mels = log_mel.shape |
| input_name = session.get_inputs()[0].name |
|
|
| |
| if T < frame_len: |
| pad = np.zeros((frame_len - T, n_mels), dtype=np.float32) |
| log_mel = np.concatenate([log_mel, pad], axis=0) |
| T = frame_len |
|
|
| |
| embeddings = [] |
| for start in range(0, T - frame_len + 1, hop_frames): |
| window = log_mel[start: start + frame_len] |
| inp = window[np.newaxis, :, :, np.newaxis] |
| out = session.run(None, {input_name: inp}) |
| embeddings.append(out[0].flatten()) |
|
|
| if not embeddings: |
| print(f"Warning: no windows extracted from {wav_path}") |
| return None |
|
|
| |
| return np.mean(embeddings, axis=0).astype(np.float32) |
|
|
| except Exception as e: |
| print(f"Error processing {wav_path}: {e}") |
| return None |
|
|