"""Deterministic per-suspect voice assignment. A suspect maps to a stable Supertonic speaker (M1-M5 for men, F1-F5 for women) and prosody so they always sound the same - distinct cast voices from one on-device model. """ from __future__ import annotations import hashlib from ..schemas.suspect import Suspect, VoiceAssignment def _seed(*parts: str) -> int: return int.from_bytes(hashlib.sha256("|".join(parts).encode("utf-8")).digest()[:4], "big") def assign_voice(suspect: Suspect, *, engine: str = "supertonic") -> VoiceAssignment: """Assign a stable, GENDER-MATCHED voice. The Supertonic provider maps speaker_id 0-4 to male voices (M1-M5) and 5-9 to female voices (F1-F5).""" seed = _seed(suspect.sus_id, suspect.name) gender = ((suspect.visual.gender if suspect.visual else "") or "").lower() if gender.startswith("f"): speaker_id = 5 + (seed % 5) # female voices F1-F5 elif gender.startswith("m"): speaker_id = seed % 5 # male voices M1-M5 else: speaker_id = seed % 10 # unknown -> any of the 10 # Calmer, slower delivery for high-composure suspects; edgier for evasive ones. length_scale = round(0.95 + (seed % 20) / 100.0, 3) # 0.95 - 1.15 noise_w = round(0.70 + ((seed >> 8) % 20) / 100.0, 3) # 0.70 - 0.90 return VoiceAssignment( engine=engine, speaker_id=speaker_id, length_scale=length_scale, noise_w=noise_w, )