use crate::error::TalkError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LangSource {
Cli,
Config,
Detected,
}
impl LangSource {
pub fn describe(self) -> &'static str {
match self {
LangSource::Cli => "from --lang",
LangSource::Config => "from config",
LangSource::Detected => "auto-detected from text",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VoiceMeta {
pub name: String,
pub lang: String,
}
pub fn primary_subtag(code: &str) -> String {
code.split(['-', '_'])
.next()
.unwrap_or(code)
.to_ascii_lowercase()
}
pub fn guard_voice_lang(
explicit_voice: Option<&VoiceMeta>,
resolved_lang: &str,
lang_source: LangSource,
force: bool,
) -> Result<(), TalkError> {
if force {
return Ok(());
}
let Some(voice) = explicit_voice else {
return Ok(());
};
let voice_primary = primary_subtag(&voice.lang);
let resolved_primary = primary_subtag(resolved_lang);
if voice_primary == resolved_primary {
return Ok(());
}
let lang_remedy = match lang_source {
LangSource::Detected => format!(
"pass --lang {} (or the correct language) if auto-detection guessed wrong",
voice_primary
),
LangSource::Cli | LangSource::Config => format!(
"change the resolved language (currently {}) to {}",
resolved_primary, voice_primary
),
};
Err(TalkError::Config(format!(
"voice/language mismatch: the voice '{name}' speaks {voice_lang} \
but the resolved language is '{resolved}' ({how}).\n\
Remedies:\n \
- {lang_remedy};\n \
- pass a --voice whose language is '{resolved}';\n \
- pass --force to synthesize anyway with '{name}'.",
name = voice.name,
voice_lang = voice.lang,
resolved = resolved_primary,
how = lang_source.describe(),
lang_remedy = lang_remedy,
)))
}
#[cfg(test)]
mod tests {
use super::*;
fn meta(name: &str, lang: &str) -> VoiceMeta {
VoiceMeta {
name: name.to_string(),
lang: lang.to_string(),
}
}
#[test]
fn primary_subtag_strips_region_and_lowercases() {
assert_eq!(primary_subtag("en_us"), "en");
assert_eq!(primary_subtag("en-GB"), "en");
assert_eq!(primary_subtag("fr_FR"), "fr");
assert_eq!(primary_subtag("fr"), "fr");
assert_eq!(primary_subtag("ZH"), "zh");
}
#[test]
fn guard_passes_when_no_explicit_voice() {
guard_voice_lang(None, "fr", LangSource::Detected, false).expect("no voice, no guard");
}
#[test]
fn guard_passes_when_languages_match_by_primary_subtag() {
let v = meta("Paul - Neutral", "en_us");
guard_voice_lang(Some(&v), "en", LangSource::Detected, false).expect("en_us matches en");
}
#[test]
fn guard_passes_when_forced() {
let v = meta("Paul - Neutral", "en_us");
guard_voice_lang(Some(&v), "fr", LangSource::Detected, true).expect("force bypasses");
}
#[test]
fn guard_errors_on_mismatch_naming_everything() {
let v = meta("Paul - Neutral", "en_us");
let err = guard_voice_lang(Some(&v), "fr", LangSource::Detected, false)
.expect_err("must error on mismatch");
let msg = err.to_string();
assert!(msg.contains("Paul - Neutral"), "msg: {msg}");
assert!(msg.contains("en_us"), "msg: {msg}");
assert!(msg.contains("'fr'"), "msg: {msg}");
assert!(msg.contains("auto-detected from text"), "msg: {msg}");
assert!(msg.contains("--lang"), "msg: {msg}");
assert!(msg.contains("--voice"), "msg: {msg}");
assert!(msg.contains("--force"), "msg: {msg}");
}
#[test]
fn guard_error_remedy_reflects_lang_source_cli() {
let v = meta("ff_siwis", "fr");
let err = guard_voice_lang(Some(&v), "en", LangSource::Cli, false)
.expect_err("must error on mismatch");
let msg = err.to_string();
assert!(msg.contains("from --lang"), "msg: {msg}");
assert!(msg.contains("change the resolved language"), "msg: {msg}");
}
#[test]
fn guard_error_remedy_reflects_lang_source_config() {
let v = meta("ff_siwis", "fr");
let err = guard_voice_lang(Some(&v), "en", LangSource::Config, false)
.expect_err("must error on mismatch");
assert!(err.to_string().contains("from config"));
}
}