twi-symptom-classifier / utils /data_generation.py
michsethowusu's picture
Upload utils/data_generation.py
02b0ab9 verified
Raw
History Blame Contribute Delete
4.59 kB
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)
# Print model input shape so we can confirm expectations
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 # β†’ (time_frames, 32)
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, 32)
T, n_mels = log_mel.shape
input_name = session.get_inputs()[0].name
# Pad if the clip is shorter than one window
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
# Slide window and collect embeddings
embeddings = []
for start in range(0, T - frame_len + 1, hop_frames):
window = log_mel[start: start + frame_len] # (76, 32)
inp = window[np.newaxis, :, :, np.newaxis] # (1, 76, 32, 1)
out = session.run(None, {input_name: inp}) # [(1, emb_dim)]
embeddings.append(out[0].flatten())
if not embeddings:
print(f"Warning: no windows extracted from {wav_path}")
return None
# Average across windows β†’ single fixed-size embedding
return np.mean(embeddings, axis=0).astype(np.float32)
except Exception as e:
print(f"Error processing {wav_path}: {e}")
return None