hedrekao
HF deploy: clean snapshot without local artifacts
a361db3
Raw
History Blame Contribute Delete
2.39 kB
"""Gender classification based on fundamental frequency (F0)."""
import logging
import numpy as np
import librosa
from typing import Tuple
log = logging.getLogger(__name__)
def estimate_gender(signal: np.ndarray, sr: int) -> Tuple[str, float]:
"""
Estimate speaker gender from fundamental frequency (F0).
Typical F0 ranges:
- Male: 70-180 Hz (median ~120 Hz)
- Female: 160-255 Hz (median ~210 Hz)
- Ambiguous: 140-185 Hz
Args:
signal: Audio signal
sr: Sample rate in Hz
Returns:
Tuple of (gender_string, median_f0_hz)
- gender_string: "male", "female", "ambiguous", or "unknown"
- median_f0_hz: Median F0 value or None if too short
"""
min_samples = 1024
if len(signal) < min_samples:
log.warning(f"Signal too short ({len(signal)} samples) for F0 estimation")
return "unknown", None
# Resample to 16kHz if needed (standard for pitch estimation)
if sr != 16000:
signal_16k = librosa.resample(signal, orig_sr=sr, target_sr=16000)
sr_work = 16000
else:
signal_16k = signal
sr_work = sr
try:
# Use probabilistic YIN (pYIN) for robust F0 estimation
f0, voiced_flag, _ = librosa.pyin(
signal_16k.astype(np.float32),
fmin=librosa.note_to_hz("C2"), # ~65 Hz
fmax=librosa.note_to_hz("C6"), # ~1047 Hz
sr=sr_work,
frame_length=2048, # CRITICAL: must be set for proper operation
)
# Extract only voiced frames
voiced_f0 = f0[voiced_flag]
if len(voiced_f0) < 10:
log.warning(f"Insufficient voiced frames ({len(voiced_f0)}) for gender estimation")
return "unknown", None
# Use median F0 (more robust than mean to outliers)
median_f0 = float(np.nanmedian(voiced_f0))
# Classification thresholds based on gender-specific F0 ranges
if median_f0 < 140:
gender = "male"
elif median_f0 > 185:
gender = "female"
else:
gender = "ambiguous"
log.debug(f"Gender: {gender}, Median F0: {median_f0:.1f} Hz")
return gender, median_f0
except Exception as e:
log.warning(f"Pitch estimation failed: {e}")
return "unknown", None