"""Auto-casting : attribue une voix distincte a chaque personnage. Strategie deterministe : - Narrateur : voix FR native par defaut (ff_siwis), sinon premiere voix. - Personnages : voix du meme genre, distinctes tant qu'il en reste ; au-dela on recycle en repartissant le plus equitablement possible. Genre inconnu -> pool mixte. L'ordre (tri par nom) garantit la reproductibilite. L'utilisateur pourra surcharger ces choix dans l'UI. """ from __future__ import annotations from collections import Counter from typing import Optional from ..models import Cast, Character, Voicebank # Voix narrateur preferee (FR native). PREFERRED_NARRATOR = "fr_f_siwis" def _pick_pool(vb: Voicebank, gender: Optional[str], narrator_id: str) -> list[str]: """Voix candidates : on privilegie STRICTEMENT le genre (quitte a reutiliser). On ne croise le genre que si aucune voix du bon genre n'existe. Le narrateur est exclu tant qu'il reste d'autres options, pour le distinguer. """ same = [e.id for e in vb.by_gender(gender)] if gender in ("male", "female") else [] pool = same if same else [e.id for e in vb.entries] non_narrator = [vid for vid in pool if vid != narrator_id] return non_narrator or pool # garde le narrateur seulement s'il est seul def assign_voices( characters: list[Character], vb: Voicebank, *, narrator_voice_id: Optional[str] = None, respect_existing: bool = False, ) -> Cast: """Renvoie un Cast avec narrateur + voix par personnage (mutation des chars). `respect_existing=True` conserve les voix deja attribuees (overrides UI) ; sinon tout est re-calcule (auto-casting frais). """ if not vb.entries: return Cast(narrator_voice_id=narrator_voice_id, characters=characters) narrator_id = narrator_voice_id or ( PREFERRED_NARRATOR if vb.by_id(PREFERRED_NARRATOR) else vb.entries[0].id) usage: Counter[str] = Counter() usage[narrator_id] += 1 # le narrateur compte deja for ch in sorted(characters, key=lambda c: c.name.lower()): if respect_existing and ch.voice_id and vb.by_id(ch.voice_id): usage[ch.voice_id] += 1 continue # respecte une attribution existante (override utilisateur) pool = _pick_pool(vb, ch.gender, narrator_id) # Choisit la voix la moins utilisee du pool (donc une voix neuve d'abord). best = min(pool, key=lambda vid: (usage[vid], pool.index(vid))) ch.voice_id = best usage[best] += 1 return Cast(narrator_voice_id=narrator_id, characters=characters) def resolve_speaker_voice( speaker: str, cast: Cast, vb: Voicebank ) -> Optional[str]: """Mappe un nom de locuteur (segment) vers un id de voix. Matche d'abord par nom/alias exact (rapide), puis en dernier recours par rapprochement heuristique de tokens (ex: un "Jim" qui n'aurait pas encore ete absorbe comme alias de "James Holden"). """ if speaker == "narrateur": return cast.narrator_voice_id low = speaker.lower() for ch in cast.characters: if ch.name.lower() == low or low in (a.lower() for a in ch.aliases): return ch.voice_id from .dedup import heuristic_match match = heuristic_match(speaker, cast.characters) if isinstance(match, Character): return match.voice_id return None # inconnu -> le rendu repliera sur le narrateur