tts-voice-creator-clone-and.../core/seed_score.py
mARTin-B78 b36423457b feat(design): auto-pick the best seed by scored candidates
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:37:58 +02:00

73 lines
2.8 KiB
Python

"""Seed auto-pick scoring: combine ASR accuracy, loudness/clipping and audio problems into 0-100.
score = weighted mean of the available sub-scores (each 0-100). Without ASR the
accuracy weight is dropped and the rest re-normalised ("degraded" result).
accuracy = ASR round-trip word accuracy (routes.stt._word_accuracy)
loudness = 100 - |dBFS - target| * LOUD_PEN_PER_DB (capped) - CLIP_PEN if peak hits full scale
problems = 100 - sum of per-kind penalties from core.audio.audio_problems
"""
from __future__ import annotations
W_ACCURACY = 0.5
W_LOUDNESS = 0.2
W_PROBLEMS = 0.3
LOUD_PEN_PER_DB = 4.0 # points lost per dB away from the target level
LOUD_PEN_MAX = 60.0
CLIP_PEN = 30.0 # peak at/above CLIP_PEAK_DBFS
CLIP_PEAK_DBFS = -0.1
PROBLEM_PENALTY = {"silence": 25.0, "level_jump": 20.0, "clipping": 10.0}
PROBLEM_PENALTY_DEFAULT = 10.0
DEFAULT_COUNT = 8
MAX_COUNT = 16
def loudness_score(info: dict) -> float:
dbfs, peak, target = info.get("dbfs"), info.get("peak_dbfs"), info.get("target_dbfs", -20.0)
if dbfs is None:
return 0.0
s = 100.0 - min(LOUD_PEN_MAX, abs(dbfs - target) * LOUD_PEN_PER_DB)
if peak is not None and peak >= CLIP_PEAK_DBFS:
s -= CLIP_PEN
return max(0.0, s)
def problems_score(problems: list[dict]) -> float:
pen = sum(PROBLEM_PENALTY.get(p.get("kind"), PROBLEM_PENALTY_DEFAULT) for p in problems)
return max(0.0, 100.0 - pen)
def combine(accuracy: float | None, loudness: float, problems: float) -> float:
parts = [(W_LOUDNESS, loudness), (W_PROBLEMS, problems)]
if accuracy is not None:
parts.append((W_ACCURACY, accuracy))
return round(sum(w * v for w, v in parts) / sum(w for w, _ in parts), 1)
def pick_winner(candidates: list[dict]) -> int | None:
"""Seed with the highest score (ties: lowest seed); errored candidates are skipped."""
ok = [c for c in candidates if not c.get("error") and c.get("score") is not None]
return min(ok, key=lambda c: (-c["score"], c["seed"]))["seed"] if ok else None
def seed_list(seeds, count) -> list[int]:
"""Validate the request: explicit seeds win, else 1..count; bounded to MAX_COUNT."""
if seeds is not None:
if not isinstance(seeds, list) or not seeds:
raise ValueError("seeds must be a non-empty list")
out = []
for s in seeds:
if isinstance(s, bool) or not isinstance(s, int) or not 0 <= s <= 9999:
raise ValueError("seeds must be integers 0-9999")
if s not in out:
out.append(s)
if len(out) > MAX_COUNT:
raise ValueError(f"at most {MAX_COUNT} seeds")
return out
n = DEFAULT_COUNT if count is None else count
if isinstance(n, bool) or not isinstance(n, int) or not 1 <= n <= MAX_COUNT:
raise ValueError(f"count must be 1-{MAX_COUNT}")
return list(range(1, n + 1))