audiovec / predict.py
lothnic's picture
Update README with download link and inference examples
c729991 verified
Raw History Blame Contribute Delete
4.93 kB
"""Inference utilities: predict emotion and extract embeddings from audio files (PyTorch)."""
from pathlib import Path
from typing import Optional
import numpy as np
import torch
from audiovec.config import MAX_PAD_LEN, INPUT_SHAPE, EMOTION_MAPPING
from audiovec.data import load_and_process_audio
from audiovec.model import CRNNTransformer, build_transformer_model
HF_REPO_ID = "lothnic/audiovec"
HF_MODEL_PATH = "models/audiovec_model.pt"
def ensure_model(model_path: str | Path = HF_MODEL_PATH) -> Path:
"""Ensure the trained model exists locally, downloading from HF Hub if needed.
In a Hugging Face Space, the model is stored in the Space repo (uploaded via
the API). This function downloads it on first run if it's not already present.
"""
model_path = Path(model_path)
if model_path.exists():
return model_path
# Download from Hugging Face Hub (the model is stored in the Space repo)
try:
from huggingface_hub import hf_hub_download
print(f"Downloading model from {HF_REPO_ID}...")
# Strip the models/ prefix if present since the HF repo stores the file at root
hf_filename = HF_MODEL_PATH
if hf_filename.startswith("models/"):
hf_filename = hf_filename[len("models/"):]
downloaded = hf_hub_download(
repo_id=HF_REPO_ID,
filename=hf_filename,
repo_type="space",
local_dir=model_path.parent,
local_dir_use_symlinks=False,
)
print("Model downloaded successfully.")
return Path(downloaded)
except Exception as e:
raise FileNotFoundError(
f"Model not found at {model_path} and download failed: {e}. "
f"Please train the model first with `uv run python train_crnn.py`"
) from e
def load_trained_model(
model_path: str | Path,
device: torch.device | str | None = None,
) -> CRNNTransformer:
"""Load a trained model from disk, downloading first if needed."""
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model_path = ensure_model(model_path)
# Build model architecture — CRNN-Transformer (default)
model = build_transformer_model(
input_shape=INPUT_SHAPE,
embedding_dim=256,
num_classes=8,
)
# Load state dict
state = torch.load(str(model_path), map_location=device, weights_only=True)
model.load_state_dict(state)
model.to(device)
model.eval()
return model
def predict_emotion(
model: CRNNTransformer,
spectrogram: np.ndarray,
device: torch.device | str | None = None,
) -> tuple[str, float, np.ndarray]:
"""Predict emotion from a single spectrogram sample.
Returns
-------
emotion_label : str
Predicted emotion name (e.g. "happy").
confidence : float
Confidence score for the predicted emotion.
all_probs : np.ndarray, shape (num_classes,)
Probability distribution over all emotion classes.
"""
if device is None:
device = next(model.parameters()).device
# Add batch dimension: (1, 128, 228, 1)
batch = torch.from_numpy(spectrogram[np.newaxis, ...]).float().to(device)
with torch.no_grad():
logits = model(batch)
probs = torch.softmax(logits, dim=-1)[0] # shape (num_classes,)
probs_np = probs.cpu().numpy()
pred_idx = int(np.argmax(probs_np))
confidence = float(probs_np[pred_idx])
# Map index back to emotion label (indices 0-7 → mapping 1-8)
emotion_label = EMOTION_MAPPING.get(pred_idx + 1, "unknown")
return emotion_label, confidence, probs_np
def extract_embedding(
model: CRNNTransformer,
spectrogram: np.ndarray,
device: torch.device | str | None = None,
) -> np.ndarray:
"""Extract the 256-dimensional embedding vector from a spectrogram.
Returns
-------
np.ndarray, shape (256,)
"""
if device is None:
device = next(model.parameters()).device
batch = torch.from_numpy(spectrogram[np.newaxis, ...]).float().to(device)
with torch.no_grad():
embedding = model(batch, return_embedding=True)[0]
return embedding.cpu().numpy()
def predict_from_file(
model_path: str | Path,
audio_path: str | Path,
) -> dict:
"""Full inference pipeline: load model, process audio, predict + embed.
Returns
-------
dict with keys:
emotion, confidence, probabilities, embedding, spectrogram
"""
model = load_trained_model(model_path)
spectrogram = load_and_process_audio(audio_path, max_pad_len=MAX_PAD_LEN)
emotion, confidence, probs = predict_emotion(model, spectrogram)
embedding = extract_embedding(model, spectrogram)
return {
"emotion": emotion,
"confidence": confidence,
"probabilities": probs,
"embedding": embedding,
"spectrogram": spectrogram,
}