case0 / src /case_zero /tts /assignment.py
HusseinEid's picture
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55
"""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,
)