| import numpy as np |
|
|
| from . import features, fusion, models |
| from .config import SR |
| from .schema import Result |
|
|
|
|
| def analyze_one(path: str) -> Result: |
| y = features.load_audio(path) |
| feat = features.extract(y) |
|
|
| speech_segs = models.speech_segments(y) |
| snr_gap_db, longest_gap_s = _gap_stats(y, speech_segs, feat.duration_s) |
|
|
| ser_categorical = models.categorical_emotion(y) |
| dim_emotion = models.dimensional_emotion(y) |
|
|
| tags = models.noise_tags(y) |
| noise_type = models.noise_type_from_tags(tags) if tags else "" |
| if not noise_type and snr_gap_db is not None and snr_gap_db < 18: |
| noise_type = "background noise (unspecified)" |
|
|
| overlap_s = models.overlap_seconds(path) |
|
|
| return fusion.fuse( |
| feat=feat, |
| snr_gap_db=snr_gap_db, |
| ser_categorical=ser_categorical, |
| dimensional_emotion=dim_emotion, |
| noise_type=noise_type, |
| overlap_s=overlap_s, |
| longest_gap_s=longest_gap_s, |
| ) |
|
|
|
|
| def _gap_stats( |
| y: np.ndarray, speech_segs: list[tuple[float, float]] | None, duration_s: float |
| ) -> tuple[float | None, float]: |
| if not speech_segs: |
| return None, 0.0 |
|
|
| speech_mask = np.zeros(len(y), dtype=bool) |
| for start_s, end_s in speech_segs: |
| speech_mask[int(start_s * SR) : int(end_s * SR)] = True |
|
|
| gap = y[~speech_mask] |
| speech = y[speech_mask] |
| gap_rms = float(np.sqrt(np.mean(gap**2))) if len(gap) else 0.0 |
| speech_rms = float(np.sqrt(np.mean(speech**2))) if len(speech) else 0.0 |
| snr_gap_db = 20 * np.log10((speech_rms + 1e-8) / (gap_rms + 1e-8)) |
|
|
| longest_gap_s = 0.0 |
| prev_end = 0.0 |
| for start_s, end_s in speech_segs: |
| longest_gap_s = max(longest_gap_s, start_s - prev_end) |
| prev_end = end_s |
| longest_gap_s = max(longest_gap_s, duration_s - prev_end) |
|
|
| return snr_gap_db, longest_gap_s |
|
|