File size: 1,444 Bytes
414dc55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"""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,
    )