autoace / src /features.py
edwixx's picture
Deploy AutoAce voice tone dashboard
c3eb7ea verified
Raw
History Blame Contribute Delete
2.38 kB
from dataclasses import dataclass
import librosa
import numpy as np
from .config import SR
@dataclass
class AcousticFeatures:
duration_s: float
rms_mean: float
rms_std: float
pitch_mean: float
pitch_std: float
zcr_mean: float
spectral_flatness_mean: float
clipping_ratio: float
snr_estimate_db: float
silence_ratio: float
longest_silence_s: float
def load_audio(path: str) -> np.ndarray:
y, _ = librosa.load(path, sr=SR, mono=True)
return y
def extract(y: np.ndarray) -> AcousticFeatures:
duration_s = len(y) / SR
rms = librosa.feature.rms(y=y)[0]
rms_mean, rms_std = float(np.mean(rms)), float(np.std(rms))
f0, voiced_flag, _ = librosa.pyin(
y, fmin=librosa.note_to_hz("C2"), fmax=librosa.note_to_hz("C7"), sr=SR
)
voiced_f0 = f0[voiced_flag] if voiced_flag is not None else np.array([])
pitch_mean = float(np.nanmean(voiced_f0)) if voiced_f0.size else 0.0
pitch_std = float(np.nanstd(voiced_f0)) if voiced_f0.size else 0.0
zcr_mean = float(np.mean(librosa.feature.zero_crossing_rate(y)[0]))
flatness_mean = float(np.mean(librosa.feature.spectral_flatness(y=y)[0]))
clipping_ratio = float(np.mean(np.abs(y) > 0.99))
frame_rms = rms
sorted_rms = np.sort(frame_rms)
noise_floor = np.mean(sorted_rms[: max(1, len(sorted_rms) // 5)]) + 1e-8
signal_level = np.mean(sorted_rms[-max(1, len(sorted_rms) // 10) :]) + 1e-8
snr_estimate_db = float(20 * np.log10(signal_level / noise_floor))
hop_length = 512
silence_mask = frame_rms < (0.1 * (np.max(frame_rms) + 1e-8))
silence_ratio = float(np.mean(silence_mask))
longest_silence_s = _longest_run_seconds(silence_mask, hop_length)
return AcousticFeatures(
duration_s=duration_s,
rms_mean=rms_mean,
rms_std=rms_std,
pitch_mean=pitch_mean,
pitch_std=pitch_std,
zcr_mean=zcr_mean,
spectral_flatness_mean=flatness_mean,
clipping_ratio=clipping_ratio,
snr_estimate_db=snr_estimate_db,
silence_ratio=silence_ratio,
longest_silence_s=longest_silence_s,
)
def _longest_run_seconds(mask: np.ndarray, hop_length: int) -> float:
longest = current = 0
for v in mask:
current = current + 1 if v else 0
longest = max(longest, current)
return float(longest * hop_length / SR)