Source code for speaker_helper.eval.priors

"""
Quality priors and Pareto selection for choosing an engine.

Module summary
--------------
Two ideas make engine choice operational:

1. **A quality axis even without a reference.** When no transcriber is wired in,
   fidelity is *estimated* from per-engine priors — order-of-magnitude scores in
   ``[0, 1]``, recalibrated from real runs. This keeps every evaluation
   reporting *both* speed and quality.
2. **Pareto selection.** To pick an engine/voice you want the non-dominated set
   on the quality↔RTF plane — high quality *and* fast. :func:`pareto_front`
   extracts it.

Together these turn a batch of :class:`~speaker_helper.eval.runner.EvalReport`
(one per engine) into an actionable "which engine should I ship?" answer.

Usage example
-------------
>>> from speaker_helper.eval.priors import engine_quality_prior
>>> engine_quality_prior("kokoro")
0.75

Author
------
Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import Protocol

# TTS quality priors in [0, 1]: order-of-magnitude estimates keyed on the
# engine — recalibrate from measured round-trips (WER/chrF) as real data
# accumulates. Unknown engines get 0.80.
TTS_QUALITY_PRIORS: dict[str, float] = {
    "kokoro": 0.75,
    "chatterbox_turbo": 0.82,
    "chatterbox": 0.90,
    "qwen3_tts": 0.88,
    "qwen_customvoice": 0.90,
    "f5": 0.86,
    "xtts": 0.70,
    "mock": 1.00,  # deterministic reference signal — nominal "perfect".
}
_DEFAULT_PRIOR = 0.80


[docs] def engine_quality_prior(engine: str) -> float: """Return the prior quality score in ``[0, 1]`` for a TTS ``engine``. Parameters ---------- engine : str Engine id (e.g. ``"kokoro"``). Returns ------- float The inherited prior, or ``0.80`` for an unknown engine. """ return TTS_QUALITY_PRIORS.get(engine, _DEFAULT_PRIOR)
class _ParetoPoint(Protocol): """A candidate carrying a quality (maximise) and an RTF (minimise).""" @property def quality(self) -> float: """Quality score to maximise (higher is better).""" ... @property def mean_rtf(self) -> float: """Mean real-time factor to minimise (lower is faster).""" ...
[docs] def pareto_front(points: Sequence[_ParetoPoint]) -> list[_ParetoPoint]: """Return the non-dominated points: maximise quality, minimise RTF. A point A dominates B if A is at least as good on both axes (higher or equal ``quality``, lower or equal ``mean_rtf``) and strictly better on one. Parameters ---------- points : sequence Candidates, each exposing ``quality`` and ``mean_rtf`` (e.g. :class:`~speaker_helper.eval.runner.EvalReport`). Returns ------- list The Pareto-optimal subset, in input order. """ front: list[_ParetoPoint] = [] for a in points: # ``a`` stays on the front unless some other point dominates it. dominated = False for b in points: # A point never dominates itself. if b is a: continue # b dominates a when it is at least as good on BOTH axes (higher or # equal quality, lower or equal RTF) and strictly better on one — # i.e. b is a free improvement over a, so a is not Pareto-optimal. if ( b.quality >= a.quality and b.mean_rtf <= a.mean_rtf and (b.quality > a.quality or b.mean_rtf < a.mean_rtf) ): dominated = True break # Undominated points form the quality↔RTF frontier. if not dominated: front.append(a) return front