73 lines
2.8 KiB
Python
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))
|