use crate::error::TalkError;
pub(crate) const SPEAKERS: &[(i32, &str)] = &[
(0, "af_alloy"),
(1, "af_aoede"),
(2, "af_bella"),
(3, "af_heart"),
(4, "af_jessica"),
(5, "af_kore"),
(6, "af_nicole"),
(7, "af_nova"),
(8, "af_river"),
(9, "af_sarah"),
(10, "af_sky"),
(11, "am_adam"),
(12, "am_echo"),
(13, "am_eric"),
(14, "am_fenrir"),
(15, "am_liam"),
(16, "am_michael"),
(17, "am_onyx"),
(18, "am_puck"),
(19, "am_santa"),
(20, "bf_alice"),
(21, "bf_emma"),
(22, "bf_isabella"),
(23, "bf_lily"),
(24, "bm_daniel"),
(25, "bm_fable"),
(26, "bm_george"),
(27, "bm_lewis"),
(28, "ef_dora"),
(29, "em_alex"),
(30, "ff_siwis"),
(31, "hf_alpha"),
(32, "hf_beta"),
(33, "hm_omega"),
(34, "hm_psi"),
(35, "if_sara"),
(36, "im_nicola"),
(37, "jf_alpha"),
(38, "jf_gongitsune"),
(39, "jf_nezumi"),
(40, "jf_tebukuro"),
(41, "jm_kumo"),
(42, "pf_dora"),
(43, "pm_alex"),
(44, "pm_santa"),
(45, "zf_xiaobei"),
(46, "zf_xiaoni"),
(47, "zf_xiaoxiao"),
(48, "zf_xiaoyi"),
(49, "zm_yunjian"),
(50, "zm_yunxi"),
(51, "zm_yunxia"),
(52, "zm_yunyang"),
];
const LANG_VOICE_PREFIXES: &[(&str, &[&str])] = &[
("en", &["af", "am", "bf", "bm"]), ("es", &["ef", "em"]), ("fr", &["ff"]), ("hi", &["hf", "hm"]), ("it", &["if", "im"]), ("ja", &["jf", "jm"]), ("pt", &["pf", "pm"]), ("zh", &["zf", "zm"]), ];
pub(crate) fn name_to_sid(name: &str) -> Result<i32, TalkError> {
let trimmed = name.trim();
if let Ok(sid) = trimmed.parse::<i32>() {
if SPEAKERS.iter().any(|(i, _)| *i == sid) {
return Ok(sid);
}
return Err(TalkError::Config(format!(
"kokoro voice sid {} out of range (valid: 0-{})",
sid,
SPEAKERS.len() - 1
)));
}
if let Some((sid, _)) = SPEAKERS.iter().find(|(_, n)| *n == trimmed) {
return Ok(*sid);
}
Err(TalkError::Config(format!(
"unknown kokoro voice '{}'. Examples: af_heart, am_michael, \
ff_siwis. See the model card for all 53 voices.",
trimmed
)))
}
pub(crate) fn default_sid_for_lang(lang: &str) -> i32 {
let primary = lang.split('-').next().unwrap_or(lang).to_ascii_lowercase();
if let Some((_, prefixes)) = LANG_VOICE_PREFIXES.iter().find(|(l, _)| *l == primary) {
for (sid, name) in SPEAKERS {
let name_prefix = name.split('_').next().unwrap_or("");
if prefixes.contains(&name_prefix) {
return *sid;
}
}
}
SPEAKERS.first().map(|(sid, _)| *sid).unwrap_or(0)
}
pub(crate) fn voice_language(name: &str) -> Option<crate::synthesis::resolve::VoiceMeta> {
let trimmed = name.trim();
let resolved_name = if let Ok(sid) = trimmed.parse::<i32>() {
SPEAKERS.iter().find(|(i, _)| *i == sid).map(|(_, n)| *n)?
} else if SPEAKERS.iter().any(|(_, n)| *n == trimmed) {
trimmed
} else {
return None;
};
let prefix = resolved_name.split('_').next().unwrap_or("");
let lang = LANG_VOICE_PREFIXES
.iter()
.find(|(_, prefixes)| prefixes.contains(&prefix))
.map(|(l, _)| *l)?;
Some(crate::synthesis::resolve::VoiceMeta {
name: resolved_name.to_string(),
lang: lang.to_string(),
})
}
pub(crate) fn default_voice_for_lang(lang: &str) -> Option<&'static str> {
let primary = lang
.split(['-', '_'])
.next()
.unwrap_or(lang)
.to_ascii_lowercase();
let (_, prefixes) = LANG_VOICE_PREFIXES.iter().find(|(l, _)| *l == primary)?;
SPEAKERS
.iter()
.find(|(_, name)| prefixes.contains(&name.split('_').next().unwrap_or("")))
.map(|(_, name)| *name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_has_53_speakers_with_contiguous_ids() {
assert_eq!(SPEAKERS.len(), 53);
for (idx, (sid, _)) in SPEAKERS.iter().enumerate() {
assert_eq!(*sid, idx as i32, "sid must equal index");
}
}
#[test]
fn name_to_sid_known_names() {
assert_eq!(name_to_sid("af_heart").unwrap(), 3);
assert_eq!(name_to_sid("am_michael").unwrap(), 16);
assert_eq!(name_to_sid("ff_siwis").unwrap(), 30);
assert_eq!(name_to_sid("zm_yunyang").unwrap(), 52);
}
#[test]
fn name_to_sid_trims_whitespace() {
assert_eq!(name_to_sid(" af_heart \n").unwrap(), 3);
}
#[test]
fn name_to_sid_bare_integer() {
assert_eq!(name_to_sid("0").unwrap(), 0);
assert_eq!(name_to_sid("52").unwrap(), 52);
}
#[test]
fn name_to_sid_bare_integer_out_of_range() {
let err = name_to_sid("53").expect_err("out of range");
assert!(err.to_string().contains("out of range"));
}
#[test]
fn name_to_sid_unknown_name_errors() {
let err = name_to_sid("no_such_voice").expect_err("unknown");
let msg = err.to_string();
assert!(msg.contains("no_such_voice"));
assert!(msg.contains("af_heart"));
}
#[test]
fn default_sid_by_lang_is_prefix_derived() {
assert_eq!(default_sid_for_lang("en"), 0);
assert_eq!(default_sid_for_lang("fr"), 30);
assert_eq!(default_sid_for_lang("zh"), 45);
assert_eq!(default_sid_for_lang("fr-FR"), 30);
assert_eq!(default_sid_for_lang("xx"), 0);
}
#[test]
fn voice_language_derives_prefix_language() {
assert_eq!(voice_language("af_heart").unwrap().lang, "en");
assert_eq!(voice_language("am_michael").unwrap().lang, "en");
assert_eq!(voice_language("bf_alice").unwrap().lang, "en");
let ff = voice_language("ff_siwis").unwrap();
assert_eq!(ff.lang, "fr");
assert_eq!(ff.name, "ff_siwis");
assert_eq!(voice_language("zf_xiaobei").unwrap().lang, "zh");
assert_eq!(voice_language("jf_alpha").unwrap().lang, "ja");
}
#[test]
fn voice_language_bare_sid_resolves_to_name_and_lang() {
let v = voice_language("30").unwrap();
assert_eq!(v.name, "ff_siwis");
assert_eq!(v.lang, "fr");
}
#[test]
fn voice_language_unknown_is_none() {
assert!(voice_language("no_such_voice").is_none());
assert!(voice_language("999").is_none());
}
#[test]
fn default_voice_for_lang_names() {
assert_eq!(default_voice_for_lang("en"), Some("af_alloy"));
assert_eq!(default_voice_for_lang("fr"), Some("ff_siwis"));
assert_eq!(default_voice_for_lang("fr-FR"), Some("ff_siwis"));
assert_eq!(default_voice_for_lang("zh"), Some("zf_xiaobei"));
assert_eq!(default_voice_for_lang("de"), None);
assert_eq!(default_voice_for_lang("xx"), None);
}
}