Source code for speaker_helper.voices

"""
Curated preset voices, keyed by language and gender.

Module summary
--------------
Auto-picking a voice from the engine's presets (see
resolution by language) is convenient
but blind to gender: it takes the first voice whose language matches. When an
application needs a *specific* male or female voice per language — a two-speaker
dialogue, a role-played call — it should not have to hard-code raw engine ids.

This module names that choice. :data:`PRESET_VOICES` is a small, curated set of
**real kokoro voices** (the ids the ``kokoro`` backend actually serves),
and :func:`voice_for` resolves a ``(language, gender)`` pair to one of them.

Notes on coverage
-----------------
kokoro ships several English voices of each gender and one French voice,
``ff_siwis`` (female). It has **no native French male voice**, so the French male
slot maps to a real male voice id (``bm_george``) that renders French text with a
male timbre. Every id here is a genuine kokoro preset; none is a placeholder.

Usage example
-------------
>>> from speaker_helper.voices import voice_for
>>> voice_for("fr", "female")
'ff_siwis'
>>> voice_for("en", "male")
'am_michael'

Author
------
Warith HARCHAOUI — https://linkedin.com/in/warith-harchaoui
"""

from __future__ import annotations

from speaker_helper.types import Voice

# Curated, real kokoro presets. Two English voices per gender give variety for
# multi-speaker scenes; French has its one native female voice plus a real male
# voice standing in for the gender kokoro does not natively cover.
PRESET_VOICES: tuple[Voice, ...] = (
    # English — female
    Voice(voice_id="af_heart", name="Heart", language="en", gender="female", engine="kokoro"),
    Voice(voice_id="bf_emma", name="Emma", language="en", gender="female", engine="kokoro"),
    # English — male
    Voice(voice_id="am_michael", name="Michael", language="en", gender="male", engine="kokoro"),
    Voice(voice_id="bm_george", name="George", language="en", gender="male", engine="kokoro"),
    # French — female (kokoro's native French voice)
    Voice(voice_id="ff_siwis", name="Siwis", language="fr", gender="female", engine="kokoro"),
    # French — male (no native kokoro French male; a real male voice speaks the
    # French text with a male timbre).
    Voice(voice_id="bm_george", name="George (fr)", language="fr", gender="male", engine="kokoro"),
)


[docs] def voices_for(language: str, gender: str = "") -> list[Voice]: """Return the curated voices matching ``language`` and, if given, ``gender``. Parameters ---------- language : str ISO-639-1 language code (e.g. ``"fr"``, ``"en"``). gender : str, optional ``"male"`` or ``"female"`` to filter; empty returns both genders. Returns ------- list of Voice Matching voices, in catalogue order (may be empty). """ lang = (language or "").lower()[:2] gen = (gender or "").lower() return [v for v in PRESET_VOICES if v.language == lang and (not gen or v.gender == gen)]
[docs] def voice_for(language: str, gender: str = "", *, index: int = 0) -> str: """Resolve a ``(language, gender)`` pair to a concrete kokoro voice id. Parameters ---------- language : str ISO-639-1 language code. gender : str, optional ``"male"`` or ``"female"``; empty picks the first voice of the language. index : int, optional Which matching voice to take when several exist, cycled modulo the count (so callers can hand out distinct voices to distinct speakers). Returns ------- str A real kokoro ``voice_id``. Falls back to the first voice of the language (ignoring gender), then to ``ff_siwis``/``am_michael``-style defaults, so the result is always a usable id. """ matches = voices_for(language, gender) if not matches: # No gendered match: relax to any voice of the language, then to English. matches = voices_for(language) or voices_for("en") if not matches: return "af_heart" # last-resort real kokoro id return matches[index % len(matches)].voice_id