voxsplit / backend /gender.py
Ajay
VoxSplit POC
e630855
Raw
History Blame Contribute Delete
2.87 kB
"""Gender detection using a fine-tuned wav2vec2 classifier.
Model: prithivMLmods/Common-Voice-Gender-Detection
https://huggingface.co/prithivMLmods/Common-Voice-Gender-Detection
A `facebook/wav2vec2-base-960h` model fine-tuned for binary (female/male)
speaker-gender classification. We feed it a 16 kHz mono waveform and read the
softmax probabilities. If the top probability is below a confidence floor (or
there isn't enough audio) we report "uncertain" instead of guessing.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from functools import lru_cache
import numpy as np
import torch
from transformers import Wav2Vec2FeatureExtractor, Wav2Vec2ForSequenceClassification
MODEL_NAME = "prithivMLmods/Common-Voice-Gender-Detection"
TARGET_SR = 16000
MIN_SAMPLES = int(0.3 * TARGET_SR) # need ~0.3s of audio to bother
CONFIDENCE_FLOOR = 0.6 # below this we say "uncertain"
@dataclass
class GenderResult:
label: str # "male" | "female" | "uncertain"
confidence: float # top-class softmax probability (0..1)
probs: dict = field(default_factory=dict) # {"male": p, "female": p}
audio_seconds: float = 0.0
@lru_cache(maxsize=1)
def _load():
"""Load (and cache) the model + feature extractor once per process."""
model = Wav2Vec2ForSequenceClassification.from_pretrained(MODEL_NAME)
extractor = Wav2Vec2FeatureExtractor.from_pretrained(MODEL_NAME)
model.eval()
id2label = {int(k): str(v).lower() for k, v in model.config.id2label.items()}
return model, extractor, id2label
def warmup() -> None:
"""Eagerly load the model (e.g. at server startup) to avoid a cold first request."""
_load()
def classify(samples: np.ndarray, sr: int) -> GenderResult:
"""Classify gender from a mono waveform (float32)."""
audio_seconds = float(len(samples) / sr) if sr else 0.0
if samples is None or samples.size < MIN_SAMPLES:
return GenderResult("uncertain", 0.0, {}, round(audio_seconds, 2))
samples = np.asarray(samples, dtype=np.float32)
if sr != TARGET_SR:
import librosa
samples = librosa.resample(samples, orig_sr=sr, target_sr=TARGET_SR)
sr = TARGET_SR
model, extractor, id2label = _load()
inputs = extractor(samples, sampling_rate=sr, return_tensors="pt", padding=True)
with torch.no_grad():
logits = model(**inputs).logits
probs = torch.softmax(logits, dim=1).squeeze(0).tolist()
prob_map = {id2label[i]: float(probs[i]) for i in range(len(probs))}
label = max(prob_map, key=prob_map.get)
confidence = prob_map[label]
if confidence < CONFIDENCE_FLOOR:
label = "uncertain"
return GenderResult(
label=label,
confidence=round(confidence, 3),
probs={k: round(v, 3) for k, v in prob_map.items()},
audio_seconds=round(audio_seconds, 2),
)