hackstorm_voice_model / app /audio /audio_processor.py
Frost10's picture
initail commit
2a4a312
Raw
History Blame Contribute Delete
3.66 kB
"""
Audio Processing Module (CNN Version)
- Audio conversion (mp3 / wav)
- MFCC extraction for CNN
"""
import io
import tempfile
import os
from typing import Tuple
import numpy as np
import librosa
from pydub import AudioSegment
class AudioProcessor:
# cnn parameters
SAMPLE_RATE = 16000
N_MFCC = 40
N_FFT = 1024
HOP_LENGTH = 160 # 10 ms hop
MAX_LEN = 300 # time frames (~3 sec)
MIN_AUDIO_SEC = 0.5
# Audio Conversion
def convert_audio_to_samples(self, audio_bytes: bytes) -> Tuple[np.ndarray, int]:
temp_path = None
try:
is_wav = audio_bytes[:4] in [b"RIFF", b"riff"]
suffix = ".wav" if is_wav else ".mp3"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as f:
f.write(audio_bytes)
temp_path = f.name
# Try librosa
try:
audio, sr = librosa.load(
temp_path, sr=self.SAMPLE_RATE, mono=True
)
return audio.astype(np.float32), sr
except Exception:
pass
# Try soundfile
try:
import soundfile as sf
audio, sr = sf.read(temp_path)
if audio.ndim > 1:
audio = audio.mean(axis=1)
if sr != self.SAMPLE_RATE:
audio = librosa.resample(audio, orig_sr=sr, target_sr=self.SAMPLE_RATE)
return audio.astype(np.float32), self.SAMPLE_RATE
except Exception:
pass
# Fallback: pydub (needs ffmpeg)
audio = AudioSegment.from_file(temp_path)
buf = io.BytesIO()
audio.export(buf, format="wav")
buf.seek(0)
audio, sr = librosa.load(buf, sr=self.SAMPLE_RATE, mono=True)
return audio.astype(np.float32), sr
except Exception as e:
raise ValueError(f"Audio conversion failed: {e}")
finally:
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
# CNN MFCC
def extract_mfcc_cnn(self, audio: np.ndarray, sr: int) -> np.ndarray:
min_len = int(self.MIN_AUDIO_SEC * sr)
if len(audio) < min_len:
raise ValueError("Audio too short for detection")
mfcc = librosa.feature.mfcc(
y=audio,
sr=sr,
n_mfcc=self.N_MFCC,
n_fft=self.N_FFT,
hop_length=self.HOP_LENGTH
)
# NORMALIZATION (MANDATORY)
mfcc = (mfcc - np.mean(mfcc)) / (np.std(mfcc) + 1e-6)
# Pad / trim time axis
if mfcc.shape[1] < self.MAX_LEN:
pad = self.MAX_LEN - mfcc.shape[1]
mfcc = np.pad(mfcc, ((0, 0), (0, pad)), mode="constant")
else:
mfcc = mfcc[:, :self.MAX_LEN]
# Shape → (1, 40, T)
return mfcc[np.newaxis, :, :].astype(np.float32)
def process_audio_file(self, file_bytes: bytes, filename: str) -> Tuple[dict, np.ndarray, int]:
"""Process raw audio file bytes (mp3/wav) directly — no base64 needed."""
audio, sr = self.convert_audio_to_samples(file_bytes)
features = {
'pitch_std': 50.0,
'pitch_range': 200.0,
'spectral_centroid_std': 500.0,
'rms_std': 0.05,
'zcr_std': 0.05,
'voiced_ratio': 0.5,
'mfcc_0_std': 100.0,
'delta_mfcc_0_std': 1.0,
'hf_energy_ratio': 0.1,
}
return features, audio, sr
# Singleton (use everywhere)
audio_processor = AudioProcessor()