"""Far-Field ASR Score (FFAS): harmonic mean of smooth per-condition accuracies.""" from __future__ import annotations from typing import Optional, Sequence def ffas_harmonic(values: Sequence[float], weights: Optional[Sequence[float]] = None) -> float: """ Weighted harmonic mean of positive smooth scores in (0, 1]; higher is better. Used for per-scenario accuracies and optional speed/RTF terms. """ if not values: raise ValueError("values must contain at least one entry") if any(v <= 0 for v in values): raise ValueError("values must be positive") if weights is None: weights = [1.0] * len(values) elif len(weights) != len(values): raise ValueError("weights and values must have the same length") weighted_recip_sum = sum(w / v for w, v in zip(weights, values)) return sum(weights) / weighted_recip_sum def smooth_speed_from_rtf(rtf: float) -> float: """Map RTF (audio sec / infer sec, higher = faster) to a smooth score in (0, 1].""" if rtf < 0 or rtf != rtf: # NaN raise ValueError("RTF must be a non-negative finite number") r = float(rtf) return r / (1.0 + r) def ffas(wers: Sequence[float], weights: Optional[Sequence[float]] = None) -> float: """ Far-Field ASR Score (FFAS): harmonic mean of smooth accuracies. Each WER is mapped to accuracy via A = 1 / (1 + WER), bounded in (0, 1]. Handles WER > 1 gracefully (never collapses to 0). The final score is the (weighted) harmonic mean of accuracies across all conditions. Args: wers: WER values for each condition (non-negative). weights: Optional weights, same length as wers. Defaults to equal. Returns: FFAS score in (0, 1]. Higher is better. """ if not wers: raise ValueError("wers must contain at least one value") if any(w < 0 for w in wers): raise ValueError("WER values must be non-negative") accs = [1.0 / (1.0 + wer) for wer in wers] return ffas_harmonic(accs, weights) def robustness(wer_anechoic: float, wer_degraded: float) -> float: """ Companion metric: how gracefully the model degrades. Returns A_degraded / A_anechoic. Closer to 1.0 = more robust. """ a_an = 1.0 / (1.0 + wer_anechoic) a_deg = 1.0 / (1.0 + wer_degraded) return a_deg / a_an