#![allow(clippy::wildcard_imports)]
#![allow(clippy::too_many_lines)]
use std::collections::HashMap;
use super::*;
pub(crate) fn make_silent_mp3(frames: usize) -> Vec<u8> {
let frame_len = 417; let mut mp3 = Vec::with_capacity(frames * frame_len);
for _ in 0..frames {
mp3.extend_from_slice(&[0xFF, 0xFB, 0x90, 0xC0]);
mp3.extend(std::iter::repeat_n(0u8, frame_len - 4));
}
mp3
}
struct DribbleReader {
data: Vec<u8>,
pos: usize,
piece: usize,
}
impl std::io::Read for DribbleReader {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
if self.pos >= self.data.len() {
return Ok(0);
}
std::thread::sleep(std::time::Duration::from_millis(1));
let n = out.len().min(self.piece).min(self.data.len() - self.pos);
out[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
#[test]
pub(crate) fn streaming_decode_matches_buffered_decode() {
let mp3 = make_silent_mp3(20);
let expected = decode_mp3_to_pcm16_mono(&mp3);
assert!(!expected.is_empty(), "reference decode produced audio");
let reader = DribbleReader {
data: mp3,
pos: 0,
piece: 97, };
let mut collected: Vec<u8> = Vec::new();
let mut deliveries = 0usize;
let total = stream_body_to_on_audio(reader, false, 24_000, None, &mut |ev| {
if let StreamEvt::Audio(chunk) = ev {
collected.extend_from_slice(chunk);
deliveries += 1;
}
})
.expect("stream");
assert_eq!(total, collected.len());
assert_eq!(collected, expected, "byte-identical to buffered decode");
assert!(deliveries > 1, "expected incremental delivery");
}
#[test]
pub(crate) fn streaming_pcm_passthrough_preserves_bytes() {
let pcm: Vec<u8> = (0..10_000u16).flat_map(u16::to_le_bytes).collect();
let reader = DribbleReader {
data: pcm.clone(),
pos: 0,
piece: 4096,
};
let mut collected: Vec<u8> = Vec::new();
let total = stream_body_to_on_audio(reader, true, 24_000, None, &mut |ev| {
if let StreamEvt::Audio(chunk) = ev {
collected.extend_from_slice(chunk);
}
})
.expect("stream");
assert_eq!(total, pcm.len());
assert_eq!(collected, pcm);
}
#[test]
pub(crate) fn estimated_boundaries_fire_progressively_during_streaming() {
let mp3 = make_silent_mp3(80); let plan = EstimatePlan::build("one two three four five six seven");
let expected = plan.len();
assert!(expected > 0);
let reader = DribbleReader {
data: mp3,
pos: 0,
piece: 97,
};
let mut audio_chunks = 0usize;
let mut boundaries: Vec<String> = Vec::new();
stream_body_to_on_audio(reader, false, 24_000, Some(plan), &mut |ev| match ev {
StreamEvt::Audio(_) => audio_chunks += 1,
StreamEvt::Boundary(word, ..) => boundaries.push(word.to_string()),
})
.expect("stream");
assert!(!boundaries.is_empty(), "no boundaries fired");
assert_eq!(boundaries.len(), expected);
assert!(audio_chunks > 1);
}
#[test]
pub(crate) fn estimated_boundaries_interleave_with_audio_events() {
use EventKind::{Audio as AudioEvt, Boundary as BoundaryEvt};
#[derive(PartialEq, Debug, Clone, Copy)]
enum EventKind {
Audio,
Boundary,
}
let mp3 = make_silent_mp3(80);
let plan = EstimatePlan::build("one two three four five six seven");
let reader = DribbleReader {
data: mp3,
pos: 0,
piece: 97,
};
let mut seq: Vec<EventKind> = Vec::new();
let mut record = |ev: StreamEvt<'_>| match ev {
StreamEvt::Audio(..) => seq.push(AudioEvt),
StreamEvt::Boundary(..) => seq.push(BoundaryEvt),
};
stream_body_to_on_audio(reader, false, 24_000, Some(plan), &mut record).expect("stream");
let interleaved = seq
.iter()
.enumerate()
.any(|(i, k)| *k == BoundaryEvt && seq[i + 1..].contains(&AudioEvt));
assert!(
interleaved,
"expected a boundary fired before the final audio chunk; seq = {seq:?}"
);
}
#[test]
pub(crate) fn estimate_plan_strips_ssml_before_estimating() {
let plain = EstimatePlan::build("hello world");
let ssml = EstimatePlan::build("<speak>hello <break time=\"1s\"/> world</speak>");
assert_eq!(plain.len(), ssml.len());
let words: Vec<String> = (0..ssml.len())
.map(|i| ssml.event(i).expect("in range").word.clone())
.collect();
assert_eq!(words, vec!["hello", "world"]);
assert!(ssml.event(1).expect("in range").char_offset > 0);
}
#[test]
pub(crate) fn streaming_undecodable_body_reports_error() {
let garbage = b"this is definitely not audio".to_vec();
let reader = DribbleReader {
data: garbage,
pos: 0,
piece: 4096,
};
let result = stream_body_to_on_audio(reader, false, 24_000, None, &mut |_| {});
assert!(result.is_err(), "garbage body should surface probe failure");
}
#[test]
pub(crate) fn test_build_azure_ssml() {
let ssml = build_azure_ssml("Hello world", "en-US-AriaNeural", 1.0, 1.0, 1.0);
assert!(ssml.contains("<speak"));
assert!(ssml.contains("en-US-AriaNeural"));
assert!(ssml.contains("Hello world"));
assert!(!ssml.contains("<prosody"));
}
#[test]
pub(crate) fn test_build_azure_ssml_with_prosody() {
let ssml = build_azure_ssml("Hello world", "en-US-AriaNeural", 1.5, 0.8, 1.4);
assert!(ssml.contains("<prosody"));
assert!(ssml.contains("rate=\"+50%\""));
assert!(ssml.contains("pitch=\"-10%\""));
assert!(ssml.contains("volume=\"+40%\""));
}
#[test]
pub(crate) fn test_normalize_envelope_fills_missing_attributes() {
let result = normalize_ssml_envelope(
"<speak>Repeat <mark name=\"m1\"/> test</speak>",
"en-GB-SoniaNeural",
);
let expected_prefix =
format!("<speak version=\"1.0\" xmlns=\"{SSML_XMLNS}\" xml:lang=\"en-GB\">");
assert!(
result.starts_with(&expected_prefix),
"envelope attributes must be added in order: {result}"
);
assert!(result.ends_with("Repeat <mark name=\"m1\"/> test</speak>"));
}
#[test]
pub(crate) fn test_normalize_envelope_keeps_present_attributes() {
let ssml = "<speak version=\"1.1\" xmlns=\"urn:custom\" xml:lang=\"de-DE\">Hallo</speak>";
assert_eq!(normalize_ssml_envelope(ssml, "en-US-AriaNeural"), ssml);
}
#[test]
pub(crate) fn test_normalize_envelope_fills_only_gaps() {
let result = normalize_ssml_envelope(
"<speak xml:lang='fr-FR'>Bonjour</speak>",
"en-US-AriaNeural",
);
assert!(result.contains("xml:lang='fr-FR'"), "existing lang kept");
assert!(result.contains("version=\"1.0\""), "version added");
assert!(result.contains("xmlns="), "xmlns added");
assert_eq!(result.matches("xml:lang").count(), 1);
}
#[test]
pub(crate) fn test_normalize_envelope_leaves_plain_text_and_fragments_alone() {
assert_eq!(
normalize_ssml_envelope("Angle < bracket", "en-US-AriaNeural"),
"Angle < bracket"
);
assert_eq!(
normalize_ssml_envelope("<prosody rate='slow'>hi</prosody>", "en-US-AriaNeural"),
"<prosody rate='slow'>hi</prosody>"
);
assert_eq!(
normalize_ssml_envelope("<speak version=", "en-US-AriaNeural"),
"<speak version="
);
}
#[test]
pub(crate) fn test_normalize_envelope_preserves_tag_case_and_self_closing() {
let result = normalize_ssml_envelope("<SPEAK>Hi</SPEAK>", "en-US-AriaNeural");
assert!(result.starts_with("<SPEAK version="), "tag name case kept");
assert!(result.ends_with("Hi</SPEAK>"));
let result = normalize_ssml_envelope("<speak/>", "en-US-AriaNeural");
let expected = format!("<speak version=\"1.0\" xmlns=\"{SSML_XMLNS}\" xml:lang=\"en-US\"/>");
assert_eq!(result, expected);
}
#[test]
pub(crate) fn test_normalize_envelope_composes_with_voice_injection() {
let result = inject_voice_if_missing(
&normalize_ssml_envelope("<speak>hello</speak>", "en-GB-SoniaNeural"),
"en-GB-SoniaNeural",
);
assert!(result.contains("version=\"1.0\""));
assert!(result.contains("xml:lang=\"en-GB\""));
assert!(result.contains("<voice name='en-GB-SoniaNeural'>"));
}
#[test]
pub(crate) fn test_voice_lang_from_voice_name() {
assert_eq!(voice_lang("en-GB-SoniaNeural"), "en-GB");
assert_eq!(voice_lang("en-US-AvaMultilingualNeural"), "en-US");
assert_eq!(voice_lang("alloy"), "en-US"); assert_eq!(voice_lang(""), "en-US");
}
#[test]
pub(crate) fn test_strip_marks_removes_self_closing_and_paired() {
assert_eq!(
strip_unsupported_marks("A <mark name=\"__spd_0\"/> B", false),
"A B"
);
assert_eq!(
strip_unsupported_marks("A <mark name='x'></mark> B", false),
"A B"
);
}
#[test]
pub(crate) fn test_strip_marks_leaves_similar_names_and_text_alone() {
assert_eq!(
strip_unsupported_marks("<market price='3'>", false),
"<market price='3'>"
);
assert_eq!(strip_unsupported_marks("a < b", false), "a < b");
assert_eq!(strip_unsupported_marks("<mark", false), "<mark"); assert_eq!(
strip_unsupported_marks("no marks here", false),
"no marks here"
);
}
#[test]
pub(crate) fn test_strip_marks_full_speechd_document() {
let ssml = "<speak>Hello <mark name=\"__spd_0\"/> world</speak>";
let stripped = strip_unsupported_marks(ssml, false);
assert!(!stripped.contains("mark"));
assert_eq!(stripped, "<speak>Hello world</speak>");
}
#[test]
pub(crate) fn test_bookmark_kept_for_azure_stripped_for_edge() {
assert_eq!(
strip_unsupported_marks("roses <bookmark mark='f1'/> and", false),
"roses <bookmark mark='f1'/> and"
);
assert_eq!(
strip_unsupported_marks("roses <bookmark mark='f1'/> and", true),
"roses and"
);
}
#[test]
pub(crate) fn test_express_as_kept_for_azure_tags_dropped_for_edge() {
let styled = "<mstts:express-as style=\"angry\">I am angry!</mstts:express-as>";
assert_eq!(strip_unsupported_marks(styled, false), styled);
assert_eq!(strip_unsupported_marks(styled, true), "I am angry!");
assert_eq!(
strip_unsupported_marks("<mstts:backgroundaudio src='x'/>", true),
"<mstts:backgroundaudio src='x'/>"
);
}
#[test]
pub(crate) fn test_inject_voice_when_ssml_has_no_voice_tag() {
let ssml = "<speak><prosody rate='slow'>Hello</prosody></speak>";
let result = inject_voice_if_missing(ssml, "en-GB-AbbiNeural");
assert!(
result.contains("<voice name='en-GB-AbbiNeural'>"),
"voice tag should be injected"
);
assert!(
result.contains("<prosody rate='slow'>Hello</prosody>"),
"original content should be preserved"
);
assert!(
result.contains("</voice>"),
"closing voice tag should be added"
);
}
#[test]
pub(crate) fn test_inject_voice_when_ssml_already_has_voice_tag() {
let ssml = "<speak><voice name='en-US-Aria'>Hello</voice></speak>";
let result = inject_voice_if_missing(ssml, "en-GB-AbbiNeural");
assert_eq!(result, ssml);
}
#[test]
pub(crate) fn test_inject_voice_with_empty_voice_name_is_noop() {
let ssml = "<speak>Hello</speak>";
let result = inject_voice_if_missing(ssml, "");
assert_eq!(result, ssml);
}
#[test]
pub(crate) fn test_inject_voice_with_speak_attributes() {
let ssml = "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis'>Hello</speak>";
let result = inject_voice_if_missing(ssml, "en-US-JennyNeural");
assert!(result.contains("<voice name='en-US-JennyNeural'>"));
assert!(result.contains("Hello</voice>"));
assert!(result.contains("</speak>"));
}
#[test]
pub(crate) fn test_build_azure_ssml_escapes_tags() {
let ssml = build_azure_ssml(
"<prosody rate='slow'>Hello</prosody>",
"en-US-Aria",
1.0,
1.0,
1.0,
);
assert!(
ssml.contains("<prosody"),
"build_azure_ssml must escape tags — this proves it must NOT be used for raw SSML"
);
}
#[test]
pub(crate) fn test_inject_voice_preserves_ssml_tags() {
let ssml = "<speak><prosody rate='slow'>Hello</prosody></speak>";
let result = inject_voice_if_missing(ssml, "en-US-Aria");
assert!(
!result.contains("<"),
"SSML tags must NOT be escaped in the is_ssml path"
);
assert!(
result.contains("<prosody rate='slow'>"),
"SSML tags must be preserved verbatim"
);
}
#[test]
pub(crate) fn test_build_google_request_basic() {
let (body, words) = build_google_request("Hello world", "en-US-Wavenet-D", false, None);
assert_eq!(body["input"]["text"].as_str().unwrap(), "Hello world");
assert_eq!(words.first(), None);
}
#[test]
pub(crate) fn test_build_google_request_with_ssml_override() {
let ssml = "<speak>Hello <phoneme alphabet='ipa' ph='wɜːld'>world</phoneme></speak>";
let (body, words) = build_google_request("Hello world", "en-US-Wavenet-D", false, Some(ssml));
assert!(body["input"]["ssml"].as_str().unwrap().contains("<phoneme"));
assert!(body["input"]["ssml"].as_str().unwrap().contains("wɜːld"));
assert!(
words.is_empty(),
"no mark-based words when SSML override is used"
);
}
#[test]
pub(crate) fn test_build_google_request_with_marks() {
let (body, words) = build_google_request("Hello world", "en-US-Wavenet-D", true, None);
let ssml = body["input"]["ssml"].as_str().unwrap();
assert!(ssml.contains("<mark name=\"0\"/>"));
assert!(ssml.contains("<mark name=\"1\"/>"));
assert_eq!(words.len(), 2);
assert_eq!(words[0], "Hello");
assert_eq!(words[1], "world");
assert!(body.get("enableTimePointing").is_some());
}
#[test]
pub(crate) fn test_parse_google_timepoints() {
let tps = vec![
serde_json::json!({"markName": "0", "timeSeconds": 0.125}),
serde_json::json!({"markName": "1", "timeSeconds": 0.450}),
];
let words = vec!["Hello".to_string(), "world".to_string()];
let boundaries = parse_google_timepoints(&tps, &words);
assert_eq!(boundaries.len(), 2);
assert_eq!(boundaries[0].text, "Hello");
assert_eq!(boundaries[0].offset, 125);
assert_eq!(boundaries[0].duration, 325);
assert_eq!(boundaries[1].text, "world");
assert_eq!(boundaries[1].offset, 450);
}
#[test]
pub(crate) fn test_estimate_word_boundaries() {
let boundaries = estimate_word_boundaries("Hello world this is a test");
assert_eq!(boundaries.len(), 6);
assert_eq!(boundaries[0].text, "Hello");
assert_eq!(boundaries[0].offset, 0);
assert!(boundaries[0].duration > 0);
}
#[test]
pub(crate) fn test_normalize_gender() {
assert_eq!(
crate::types::normalize_gender("Female"),
crate::types::Gender::Female
);
assert_eq!(
crate::types::normalize_gender("male"),
crate::types::Gender::Male
);
assert_eq!(
crate::types::normalize_gender(""),
crate::types::Gender::Unknown
);
}
#[test]
pub(crate) fn test_build_config_all_engines() {
let engines = [
"openai",
"elevenlabs",
"azure",
"google",
"cartesia",
"deepgram",
"playht",
"fishaudio",
"hume",
"mistral",
"murf",
"resemble",
"unrealspeech",
"upliftai",
"watson",
"witai",
"xai",
"modelslab",
];
let creds = HashMap::new();
for id in &engines {
assert!(
build_config(id, &creds).is_some(),
"Failed for engine: {id}"
);
}
}
#[test]
pub(crate) fn test_build_config_unknown() {
let creds = HashMap::new();
assert!(build_config("nonexistent", &creds).is_none());
}
#[test]
pub(crate) fn test_azure_ssml_escapes_special_chars() {
let ssml = build_azure_ssml("A & B < C > D", "en-US-AriaNeural", 1.0, 1.0, 1.0);
assert!(ssml.contains("&"));
assert!(ssml.contains("<"));
assert!(ssml.contains(">"));
}
#[test]
pub(crate) fn test_azure_ssml_escapes_voice_name() {
let ssml = build_azure_ssml("hi", "en-US-Voice'Name", 1.0, 1.0, 1.0);
assert!(
ssml.contains("'"),
"voice apostrophe should be escaped"
);
assert!(!ssml.contains("Voice'Name"));
}
#[test]
pub(crate) fn test_speech_markdown_preprocessing() {
use crate::engine::preprocess_speech_markdown;
let (result, is_ssml) =
preprocess_speech_markdown("Hello (world)[emphasis:\"strong\"]", "azure");
assert!(is_ssml);
assert!(
result.contains("<speak "),
"expected <speak> with attributes: {result}"
);
assert!(
result.contains("version="),
"missing version attr: {result}"
);
assert!(
result.contains("www.w3.org/2001/10/synthesis"),
"missing xmlns attr: {result}"
);
}
#[test]
pub(crate) fn test_watson_auth_header_format() {
use base64::Engine as _;
let api_key = "test_key_123";
let encoded = base64_encode(&format!("apikey:{api_key}"));
let auth_header = format!("Basic {encoded}");
assert!(!auth_header.ends_with(':'));
let decoded = String::from_utf8(
base64::engine::general_purpose::STANDARD
.decode(encoded.as_bytes())
.unwrap(),
)
.unwrap();
assert_eq!(decoded, format!("apikey:{api_key}"));
assert!(
!decoded.starts_with("apiKey:"),
"Watson auth must use lowercase 'apikey:' as the username, got: {decoded}"
);
}
#[test]
pub(crate) fn test_playht_config_has_user_id_header() {
let mut creds = HashMap::new();
creds.insert("userId".to_string(), "u-123".to_string());
creds.insert("apiKey".to_string(), "k".to_string());
let cfg = build_config("playht", &creds).expect("playht config");
assert_eq!(
cfg.extra_headers.get("X-User-ID").map(String::as_str),
Some("u-123")
);
assert_eq!(cfg.voice_param, "voice");
}
#[test]
pub(crate) fn test_deepgram_uses_model_param() {
let creds = HashMap::new();
let cfg = build_config("deepgram", &creds).expect("deepgram config");
assert_eq!(cfg.voice_param, "model");
}
#[test]
pub(crate) fn test_edge_config_is_credential_free_ws() {
let cfg = build_config("edge", &HashMap::new()).expect("edge config");
assert_eq!(cfg.provider_id, "edge");
assert_eq!(cfg.synth_url, "");
assert_eq!(cfg.auth_header, "");
assert!(cfg
.voices_url
.as_deref()
.unwrap()
.contains("speech.platform.bing.com"));
assert_eq!(cfg.default_voice.as_deref(), Some("en-US-AriaNeural"));
assert!(!cfg.response_is_pcm);
}
#[test]
pub(crate) fn test_edge_sec_ms_gec_is_uppercase_hex_sha256() {
let token = edge_sec_ms_gec();
assert_eq!(token.len(), 64, "token must be 64 hex chars: {token}");
assert!(
token
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()),
"token must be uppercase hex: {token}"
);
}
#[test]
pub(crate) fn test_edge_sec_ms_gec_is_stable_within_five_minutes() {
let a = edge_sec_ms_gec();
let b = edge_sec_ms_gec();
assert_eq!(a, b, "tokens within the same 5-minute window must match");
}
#[test]
pub(crate) fn test_hume_voice_is_object_in_extra_body() {
let creds = HashMap::new();
let cfg = build_config("hume", &creds).expect("hume config");
let voice = cfg
.extra_body
.get("voice")
.expect("voice key should be in extra_body");
assert!(voice.is_object(), "voice must be an object, got: {voice}");
assert!(voice.get("name").is_some());
assert_eq!(
cfg.extra_body.get("audio_format").and_then(|v| v.as_str()),
Some("wav")
);
}
#[test]
pub(crate) fn test_azure_ssml_personal_voice_embedding() {
let ssml = build_azure_ssml(
"Hello there",
"DragonLatestNeural/3059912f-2f7a-4c96-9abc-def012345678",
1.0,
1.0,
1.0,
);
assert!(
ssml.contains("xmlns:mstts='https://www.w3.org/2001/mstts'"),
"{ssml}"
);
assert!(ssml.contains("<voice name='DragonLatestNeural'>"), "{ssml}");
assert!(
ssml.contains(
"<mstts:ttsembedding \
speakerProfileId='3059912f-2f7a-4c96-9abc-def012345678'>Hello there"
),
"{ssml}"
);
}
#[test]
pub(crate) fn test_azure_ssml_normal_voice_unchanged() {
let ssml = build_azure_ssml("Hi", "en-US-AriaNeural", 1.0, 1.0, 1.0);
assert!(
ssml.contains("<voice name='en-US-AriaNeural'>Hi</voice>"),
"{ssml}"
);
assert!(!ssml.contains("mstts"), "{ssml}");
}
#[test]
pub(crate) fn test_polly_config_matrix() {
let mut creds = HashMap::new();
creds.insert("region".to_string(), "eu-west-1".to_string());
let cfg = build_config("polly", &creds).expect("polly config");
assert_eq!(
cfg.synth_url,
"https://polly.eu-west-1.amazonaws.com/v1/speech"
);
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://polly.eu-west-1.amazonaws.com/v1/voices")
);
assert_eq!(cfg.provider_id, "polly");
assert_eq!(cfg.auth_header, "");
assert_eq!(cfg.text_field, "Text");
assert_eq!(cfg.voice_param, "VoiceId");
assert_eq!(cfg.model_param.as_deref(), Some("Engine"));
assert_eq!(cfg.model_default.as_deref(), Some("neural"));
assert_eq!(cfg.default_voice.as_deref(), Some("Joanna"));
let cfg = build_config("polly", &HashMap::new()).expect("polly config");
assert!(cfg.synth_url.starts_with("https://polly.us-east-1."));
}
#[test]
pub(crate) fn test_qwen_config_matrix() {
let cfg = build_config("qwen", &engine_creds("qwen")).expect("qwen");
assert_eq!(cfg.provider_id, "qwen");
assert_eq!(
cfg.model_default.as_deref(),
Some("qwen-audio-3.0-tts-flash")
);
assert_eq!(cfg.default_voice.as_deref(), Some("longanhuan_v3.6"));
assert_eq!(cfg.synth_url, "");
assert!(cfg.voices_url.is_none());
assert!(cfg.response_is_pcm);
}
#[test]
pub(crate) fn test_qwen_model_id_override() {
let mut c = engine_creds("qwen");
c.insert("modelId".to_string(), "qwen-audio-3.0-tts-plus".to_string());
let cfg = build_config("qwen", &c).expect("qwen");
assert_eq!(
cfg.model_default.as_deref(),
Some("qwen-audio-3.0-tts-plus")
);
}
#[test]
pub(crate) fn test_qwen_ws_url_regions() {
let mut c = HashMap::new();
assert_eq!(qwen_ws_url(&c).unwrap(), QWEN_WS_URL_QWENCLOUD);
c.insert("region".to_string(), "intl".to_string());
assert_eq!(qwen_ws_url(&c).unwrap(), QWEN_WS_URL_INTL);
c.insert("region".to_string(), "beijing".to_string());
assert_eq!(qwen_ws_url(&c).unwrap(), QWEN_WS_URL_BEIJING);
c.insert("region".to_string(), "qwencloud".to_string());
assert_eq!(qwen_ws_url(&c).unwrap(), QWEN_WS_URL_QWENCLOUD);
c.insert(
"wsUrl".to_string(),
"wss://proxy.example.com/api-ws/v1/inference".to_string(),
);
assert_eq!(
qwen_ws_url(&c).unwrap(),
"wss://proxy.example.com/api-ws/v1/inference"
);
}
#[test]
pub(crate) fn test_qwen_ws_url_unknown_region_rejected() {
let mut c = HashMap::new();
c.insert("region".to_string(), "Beijing".to_string());
let err = qwen_ws_url(&c).unwrap_err().to_string();
assert!(
err.contains("unknown region 'Beijing'"),
"should name the bad region, got: {err}"
);
}
#[test]
pub(crate) fn test_qwen_sentence_clock_attributes_by_index() {
use super::qwen::QwenSentenceClock;
let mut clock = QwenSentenceClock::default();
clock.audio_frame(0, 4_800);
assert_eq!(clock.sentence_base_ms(0), 0);
clock.audio_frame(1, 2_400);
assert_eq!(clock.sentence_base_ms(0), 0);
assert_eq!(clock.sentence_base_ms(1), 100);
clock.audio_frame(1, 4_800);
assert_eq!(clock.sentence_base_ms(2), 250);
clock.audio_frame(4, 48);
assert_eq!(clock.sentence_base_ms(2), 250);
}
#[test]
pub(crate) fn test_qwen_parse_audio_frame_carries_index() {
use super::qwen::QwenServerEvent;
assert_eq!(
qwen_parse_event(
r#"{"header":{"task_id":"t","event":"result-generated"},"payload":{"output":{"type":"sentence-synthesis","sentence":{"index":3,"words":[]}}}}"#
),
QwenServerEvent::AudioFrame { sentence: 3 }
);
match qwen_parse_event(
r#"{"header":{"task_id":"t","event":"result-generated"},"payload":{"output":{"type":"sentence-end","sentence":{"index":2,"words":[]}}}}"#,
) {
QwenServerEvent::SentenceEnd { sentence, words } => {
assert_eq!(sentence, 2);
assert_eq!(words.first(), None);
}
other => panic!("expected SentenceEnd, got {other:?}"),
}
}
#[test]
pub(crate) fn test_qwen_parse_events() {
use super::qwen::QwenServerEvent;
assert_eq!(
qwen_parse_event(
r#"{"header":{"task_id":"t","event":"task-started","attributes":{}},"payload":{}}"#
),
QwenServerEvent::TaskStarted
);
assert_eq!(
qwen_parse_event(
r#"{"header":{"task_id":"t","event":"task-finished"},"payload":{"usage":{"characters":13}}}"#
),
QwenServerEvent::TaskFinished
);
assert_eq!(
qwen_parse_event(
r#"{"header":{"task_id":"t","event":"task-failed","error_code":"InvalidParameter","error_message":"bad voice"},"payload":{}}"#
),
QwenServerEvent::TaskFailed {
code: "InvalidParameter".into(),
message: "bad voice".into()
}
);
assert_eq!(
qwen_parse_event(
r#"{"header":{"task_id":"t","event":"result-generated"},"payload":{"output":{"type":"sentence-synthesis","sentence":{"index":0,"words":[]}}}}"#
),
QwenServerEvent::AudioFrame { sentence: 0 }
);
assert_eq!(
qwen_parse_event(
r#"{"header":{"task_id":"t","event":"result-generated"},"payload":{"output":{"type":"sentence-begin","original_text":"Hello"}}}"#
),
QwenServerEvent::Other
);
}
#[test]
pub(crate) fn test_qwen_parse_sentence_end_words() {
use super::qwen::{QwenServerEvent, QwenWord};
match qwen_parse_event(
r#"{"header":{"task_id":"t","event":"result-generated"},"payload":{"output":{"type":"sentence-end","sentence":{"index":0,"words":[{"text":"Before","begin_index":0,"end_index":1,"begin_time":0,"end_time":263},{"text":"my","begin_index":1,"end_index":2,"begin_time":263,"end_time":401}]},"original_text":"Before my bed"}}}"#,
) {
QwenServerEvent::SentenceEnd { words, .. } => {
assert_eq!(
words,
vec![
QwenWord {
text: "Before".into(),
begin_ms: 0,
end_ms: 263
},
QwenWord {
text: "my".into(),
begin_ms: 263,
end_ms: 401
},
]
);
}
other => panic!("expected SentenceEnd, got {other:?}"),
}
}
#[test]
pub(crate) fn test_qwen_run_task_json_shape() {
let v = qwen_run_task_json(
"task-1",
"qwen-audio-3.0-tts-flash",
"longanhuan_v3.6",
1.5,
0.25, 1.0, true,
None,
);
assert_eq!(v["header"]["action"], "run-task");
assert_eq!(v["header"]["task_id"], "task-1");
assert_eq!(v["header"]["streaming"], "duplex");
assert_eq!(v["payload"]["task_group"], "audio");
assert_eq!(v["payload"]["task"], "tts");
assert_eq!(v["payload"]["function"], "SpeechSynthesizer");
assert_eq!(v["payload"]["model"], "qwen-audio-3.0-tts-flash");
let p = &v["payload"]["parameters"];
assert_eq!(p["text_type"], "PlainText");
assert_eq!(p["voice"], "longanhuan_v3.6");
assert_eq!(p["format"], "pcm");
assert_eq!(p["sample_rate"], 24_000);
assert_eq!(p["volume"], 50);
assert_eq!(p["rate"], 1.5);
assert_eq!(p["pitch"], 0.5); assert_eq!(p["word_timestamp_enabled"], true);
assert!(p.get("instruction").is_none());
}
#[test]
pub(crate) fn test_qwen_volume_multiplier_mapping() {
let mk = |volume: f32| {
let v = qwen_run_task_json("t", "m", "v", 1.0, 1.0, volume, false, None);
v["payload"]["parameters"]["volume"].as_i64().unwrap()
};
assert_eq!(mk(0.0), 0);
assert_eq!(mk(0.5), 25);
assert_eq!(mk(1.0), 50);
assert_eq!(mk(2.0), 100);
assert_eq!(mk(5.0), 100);
}
#[test]
pub(crate) fn test_qwen_run_task_instruction_included() {
let v = qwen_run_task_json(
"t",
"m",
"v",
1.0,
1.0,
1.0,
false,
Some("Speak cheerfully."),
);
assert_eq!(
v["payload"]["parameters"]["instruction"],
"Speak cheerfully."
);
assert_eq!(v["payload"]["parameters"]["word_timestamp_enabled"], false);
}
#[test]
pub(crate) fn test_qwen_chunk_text_respects_limit() {
assert_eq!(qwen_chunk_text("hello"), vec!["hello"]);
let long = "a".repeat(40_000);
let chunks = qwen_chunk_text(&long);
assert!(chunks.len() >= 3);
assert!(chunks.iter().all(|c| c.chars().count() <= 19_000));
assert_eq!(chunks.concat(), long);
let cjk = "汉".repeat(19_050);
let chunks = qwen_chunk_text(&cjk);
assert_eq!(chunks.concat(), cjk);
for c in &chunks {
assert!(c.chars().count() <= 19_000);
}
}
pub(crate) fn engine_creds(id: &str) -> HashMap<String, String> {
let mut c = HashMap::new();
c.insert("apiKey".to_string(), "TESTKEY".to_string());
match id {
"azure" => {
c.insert("subscriptionKey".to_string(), "TESTKEY".to_string());
c.insert("region".to_string(), "eastus".to_string());
}
"watson" => {
c.insert("region".to_string(), "eu-gb".to_string());
c.insert("instanceId".to_string(), "inst-123".to_string());
}
"playht" => {
c.insert("userId".to_string(), "u-123".to_string());
}
"hume" => {
c.insert("voice".to_string(), "aoife".to_string());
}
_ => {}
}
c
}
#[test]
pub(crate) fn test_openai_config_matrix() {
let cfg = build_config("openai", &engine_creds("openai")).expect("openai");
assert_eq!(cfg.synth_url, "https://api.openai.com/v1/audio/speech");
assert_eq!(cfg.auth_header, "Authorization");
assert_eq!(cfg.auth_prefix, "Bearer ");
assert_eq!(cfg.text_field, "input");
assert_eq!(cfg.voice_param, "voice");
assert_eq!(cfg.model_param.as_deref(), Some("model"));
assert_eq!(cfg.model_default.as_deref(), Some("gpt-4o-mini-tts"));
assert_eq!(cfg.default_voice.as_deref(), Some("alloy"));
assert_eq!(cfg.provider_id, "openai");
assert!(!cfg.body_is_ssml);
}
#[test]
pub(crate) fn test_elevenlabs_config_matrix() {
let cfg = build_config("elevenlabs", &engine_creds("elevenlabs")).expect("elevenlabs");
assert!(cfg
.synth_url
.starts_with("https://api.elevenlabs.io/v1/text-to-speech/"));
assert_eq!(cfg.auth_header, "xi-api-key");
assert_eq!(cfg.auth_prefix, "");
assert_eq!(cfg.text_field, "text");
assert_eq!(cfg.model_param.as_deref(), Some("model_id"));
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://api.elevenlabs.io/v1/voices")
);
}
#[test]
pub(crate) fn test_elevenlabs_voice_id_from_creds() {
let mut c = engine_creds("elevenlabs");
c.insert("voiceId".to_string(), "v-abc".to_string());
let cfg = build_config("elevenlabs", &c).expect("elevenlabs");
assert!(cfg.synth_url.ends_with("/text-to-speech/v-abc"));
}
#[test]
pub(crate) fn test_azure_config_matrix() {
let cfg = build_config("azure", &engine_creds("azure")).expect("azure");
assert_eq!(
cfg.synth_url,
"https://eastus.tts.speech.microsoft.com/cognitiveservices/v1"
);
assert_eq!(cfg.auth_header, "Ocp-Apim-Subscription-Key");
assert_eq!(cfg.auth_prefix, "");
assert!(cfg.body_is_ssml);
assert_eq!(cfg.content_type.as_deref(), Some("application/ssml+xml"));
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://eastus.tts.speech.microsoft.com/cognitiveservices/voices/list")
);
assert_eq!(
cfg.extra_headers
.get("X-Microsoft-OutputFormat")
.map(String::as_str),
Some("raw-24khz-16bit-mono-pcm")
);
assert_eq!(cfg.default_voice.as_deref(), Some("en-US-AriaNeural"));
}
#[test]
pub(crate) fn test_azure_region_override() {
let mut c = engine_creds("azure");
c.insert("region".to_string(), "uksouth".to_string());
let cfg = build_config("azure", &c).expect("azure");
assert!(cfg.synth_url.starts_with("https://uksouth.tts."));
assert!(cfg
.voices_url
.as_deref()
.unwrap()
.starts_with("https://uksouth.tts."));
}
#[test]
pub(crate) fn test_google_config_matrix() {
let cfg = build_config("google", &engine_creds("google")).expect("google");
assert!(cfg
.synth_url
.starts_with("https://texttospeech.googleapis.com/v1/text:synthesize?key="));
assert!(cfg.synth_url.ends_with("TESTKEY"));
assert!(cfg
.voices_url
.as_deref()
.unwrap()
.starts_with("https://texttospeech.googleapis.com/v1/voices?key="));
}
#[test]
pub(crate) fn test_cartesia_config_matrix() {
let cfg = build_config("cartesia", &engine_creds("cartesia")).expect("cartesia");
assert_eq!(cfg.synth_url, "https://api.cartesia.ai/tts/bytes");
assert_eq!(cfg.auth_header, "X-API-Key");
assert_eq!(cfg.voice_param, "voice_id");
assert_eq!(cfg.model_default.as_deref(), Some("sonic-3.6"));
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://api.cartesia.ai/voices")
);
}
#[test]
pub(crate) fn test_deepgram_config_matrix() {
let cfg = build_config("deepgram", &engine_creds("deepgram")).expect("deepgram");
assert_eq!(cfg.synth_url, "https://api.deepgram.com/v1/speak");
assert_eq!(cfg.auth_header, "Authorization");
assert_eq!(cfg.auth_prefix, "Token ");
assert_eq!(cfg.voice_param, "model");
assert_eq!(cfg.default_voice.as_deref(), Some("aura-asteria-en"));
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://api.deepgram.com/v1/voices")
);
}
#[test]
pub(crate) fn test_playht_config_matrix() {
let cfg = build_config("playht", &engine_creds("playht")).expect("playht");
assert_eq!(cfg.synth_url, "https://api.play.ht/api/v2/tts");
assert_eq!(cfg.auth_header, "Authorization");
assert_eq!(cfg.auth_prefix, "Bearer ");
assert_eq!(
cfg.extra_headers.get("X-User-ID").map(String::as_str),
Some("u-123")
);
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://api.play.ht/api/v2/voices")
);
}
#[test]
pub(crate) fn test_fishaudio_config_matrix() {
let cfg = build_config("fishaudio", &engine_creds("fishaudio")).expect("fishaudio");
assert_eq!(cfg.synth_url, "https://api.fish.audio/v1/tts");
assert_eq!(cfg.auth_header, "Authorization");
assert_eq!(cfg.auth_prefix, "Bearer ");
assert_eq!(cfg.voice_param, "reference_id");
}
#[test]
pub(crate) fn test_hume_config_matrix() {
let cfg = build_config("hume", &engine_creds("hume")).expect("hume");
assert_eq!(cfg.synth_url, "https://api.hume.ai/v0/tts");
assert_eq!(cfg.auth_header, "Authorization");
assert_eq!(cfg.auth_prefix, "Bearer ");
assert_eq!(cfg.voice_param, "");
assert_eq!(
cfg.extra_body
.get("voice")
.and_then(|v| v.get("name"))
.and_then(|v| v.as_str()),
Some("aoife")
);
}
#[test]
pub(crate) fn test_mistral_config_matrix() {
let cfg = build_config("mistral", &engine_creds("mistral")).expect("mistral");
assert_eq!(cfg.synth_url, "https://api.mistral.ai/v1/tts");
assert_eq!(cfg.text_field, "text");
assert_eq!(cfg.voice_param, "voice");
}
#[test]
pub(crate) fn test_murf_config_matrix() {
let cfg = build_config("murf", &engine_creds("murf")).expect("murf");
assert_eq!(cfg.synth_url, "https://api.murf.ai/v1/speech/generate");
assert_eq!(cfg.auth_header, "api-key");
assert_eq!(cfg.auth_prefix, "");
assert_eq!(cfg.voice_param, "voice_id");
}
#[test]
pub(crate) fn test_resemble_config_matrix() {
let cfg = build_config("resemble", &engine_creds("resemble")).expect("resemble");
assert_eq!(cfg.synth_url, "https://app.resemble.ai/api/v2/synthesize");
assert_eq!(cfg.auth_header, "Authorization");
assert_eq!(cfg.auth_prefix, "Token ");
assert_eq!(cfg.voice_param, "voice_uuid");
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://app.resemble.ai/api/v2/voices")
);
}
#[test]
pub(crate) fn test_unrealspeech_config_matrix() {
let cfg = build_config("unrealspeech", &engine_creds("unrealspeech")).expect("unrealspeech");
assert_eq!(cfg.synth_url, "https://api.v7.unrealspeech.com/speech");
assert_eq!(cfg.default_voice.as_deref(), Some("Scarlett"));
assert_eq!(cfg.voice_param, "voice_id");
}
#[test]
pub(crate) fn test_upliftai_config_matrix() {
let cfg = build_config("upliftai", &engine_creds("upliftai")).expect("upliftai");
assert_eq!(cfg.synth_url, "https://api.upliftai.org/v1/tts");
}
#[test]
pub(crate) fn test_watson_config_matrix() {
let cfg = build_config("watson", &engine_creds("watson")).expect("watson");
assert!(cfg
.synth_url
.starts_with("https://eu-gb.text-to-speech.watson.cloud.ibm.com/instances/inst-123/"));
assert!(cfg.synth_url.ends_with("/v1/synthesize"));
assert_eq!(cfg.auth_header, "Authorization");
assert!(cfg.auth_prefix.starts_with("Basic "));
let encoded = cfg.auth_prefix.strip_prefix("Basic ").unwrap();
let decoded = {
use base64::Engine;
String::from_utf8(
base64::engine::general_purpose::STANDARD
.decode(encoded.as_bytes())
.unwrap(),
)
.unwrap()
};
assert_eq!(decoded, "apikey:TESTKEY");
assert!(cfg.voices_url.as_deref().unwrap().ends_with("/v1/voices"));
}
#[test]
pub(crate) fn test_witai_config_matrix() {
let cfg = build_config("witai", &engine_creds("witai")).expect("witai");
assert_eq!(cfg.synth_url, "https://api.wit.ai/synthesize?v=20240304");
assert_eq!(cfg.auth_header, "Authorization");
assert_eq!(cfg.auth_prefix, "Bearer ");
assert_eq!(
cfg.voices_url.as_deref(),
Some("https://api.wit.ai/voices?v=20240304")
);
}
#[test]
pub(crate) fn test_xai_config_matrix() {
let cfg = build_config("xai", &engine_creds("xai")).expect("xai");
assert_eq!(cfg.synth_url, "https://api.x.ai/v1/audio/speech");
assert_eq!(cfg.text_field, "input");
assert_eq!(cfg.voice_param, "voice");
}
#[test]
pub(crate) fn test_modelslab_config_matrix() {
let cfg = build_config("modelslab", &engine_creds("modelslab")).expect("modelslab");
assert_eq!(cfg.synth_url, "https://modelslab.com/api/v1/text_to_speech");
assert_eq!(cfg.auth_header, "");
}
#[test]
pub(crate) fn test_static_voice_counts() {
assert_eq!(static_voices("openai").unwrap().len(), 11);
assert_eq!(static_voices("hume").unwrap().len(), 16);
assert_eq!(static_voices("mistral").unwrap().len(), 27);
assert_eq!(static_voices("murf").unwrap().len(), 15);
assert_eq!(static_voices("unrealspeech").unwrap().len(), 8);
assert_eq!(static_voices("xai").unwrap().len(), 6);
assert_eq!(static_voices("upliftai").unwrap().len(), 4);
assert_eq!(static_voices("modelslab").unwrap().len(), 9);
}
#[test]
pub(crate) fn test_static_voices_returns_none_for_api_engines() {
assert!(static_voices("azure").is_none());
assert!(static_voices("google").is_none());
assert!(static_voices("elevenlabs").is_none());
assert!(static_voices("deepgram").is_none());
assert!(static_voices("playht").is_none());
assert!(static_voices("fishaudio").is_none());
assert!(static_voices("watson").is_none());
assert!(static_voices("witai").is_none());
assert!(static_voices("resemble").is_none());
}
#[test]
pub(crate) fn test_map_azure_voices_basic() {
let json = serde_json::json!([{
"ShortName": "en-US-AriaNeural",
"DisplayName": "Aria",
"Gender": "Female",
"Locale": "en-US",
"LocaleName": "English (United States)"
}]);
let voices = map_azure_voices(json.as_array().unwrap());
assert_eq!(voices.len(), 1);
assert_eq!(voices[0].id, "en-US-AriaNeural");
assert_eq!(voices[0].name, "Aria");
assert_eq!(voices[0].gender, crate::types::Gender::Female);
assert_eq!(voices[0].provider, "azure");
assert_eq!(voices[0].language_codes[0].bcp47, "en-US");
assert_eq!(voices[0].language_codes[0].iso639_3, "en");
}
#[test]
pub(crate) fn test_map_azure_voices_skips_missing_short_name() {
let json = serde_json::json!([
{"DisplayName": "NoShortName"},
{"ShortName": "en-US-GuyNeural", "Gender": "Male", "Locale": "en-US"}
]);
let voices = map_azure_voices(json.as_array().unwrap());
assert_eq!(voices.len(), 1);
assert_eq!(voices[0].id, "en-US-GuyNeural");
}
#[test]
pub(crate) fn test_map_google_voices_basic() {
let json = serde_json::json!([{
"name": "en-US-Wavenet-D",
"ssmlGender": "MALE",
"languageCodes": ["en-US", "en-GB"]
}]);
let voices = map_google_voices(json.as_array().unwrap());
assert_eq!(voices.len(), 1);
assert_eq!(voices[0].id, "en-US-Wavenet-D");
assert_eq!(voices[0].gender, crate::types::Gender::Male);
assert_eq!(voices[0].language_codes.len(), 2);
}
#[test]
pub(crate) fn test_map_google_voices_missing_name_skipped() {
let json = serde_json::json!([{"ssmlGender": "FEMALE"}]);
assert!(map_google_voices(json.as_array().unwrap()).is_empty());
}
#[test]
pub(crate) fn test_map_generic_voices_elevenlabs_labels_object() {
let json = serde_json::json!([{
"voice_id": "21m00Tcm4TlvDq8ikWAM",
"name": "Rachel",
"labels": {"gender": "female", "language": "en"}
}]);
let voices = map_generic_voices("elevenlabs", json.as_array().unwrap());
assert_eq!(voices.len(), 1);
assert_eq!(voices[0].id, "21m00Tcm4TlvDq8ikWAM");
assert_eq!(voices[0].name, "Rachel");
assert_eq!(voices[0].provider, "elevenlabs");
assert_eq!(voices[0].gender, crate::types::Gender::Female);
assert_eq!(voices[0].language_codes[0].bcp47, "en");
}
#[test]
pub(crate) fn test_map_generic_voices_polly_pascal_case() {
let json = serde_json::json!([{
"VoiceId": "Joanna",
"Gender": "Female",
"LanguageCode": "en-US"
}]);
let voices = map_generic_voices("polly", json.as_array().unwrap());
assert_eq!(voices.len(), 1);
assert_eq!(voices[0].id, "Joanna");
assert_eq!(voices[0].gender, crate::types::Gender::Female);
assert_eq!(voices[0].language_codes[0].bcp47, "en-US");
}
#[test]
pub(crate) fn test_map_generic_voices_cartesia_simple() {
let json = serde_json::json!([{
"id": "692f0249-6e6b-4a48-8b07-0f8f8a3f3a15",
"name": "Octopus"
}]);
let voices = map_generic_voices("cartesia", json.as_array().unwrap());
assert_eq!(voices.len(), 1);
assert_eq!(voices[0].id, "692f0249-6e6b-4a48-8b07-0f8f8a3f3a15");
assert_eq!(voices[0].name, "Octopus");
assert_eq!(voices[0].language_codes.first(), None);
}
#[test]
pub(crate) fn test_map_generic_voices_skips_no_id() {
let json = serde_json::json!([
{"category": "voiceless"}, {"id": "ok", "name": "OK"}
]);
let voices = map_generic_voices("test", json.as_array().unwrap());
assert_eq!(voices.len(), 1);
assert_eq!(voices[0].id, "ok");
}
#[test]
pub(crate) fn test_map_generic_voices_provider_tagged() {
let json = serde_json::json!([{"id": "x", "name": "X"}]);
for provider in ["openai", "murf", "resemble", "witai"] {
let voices = map_generic_voices(provider, json.as_array().unwrap());
assert_eq!(voices[0].provider, provider);
}
}
#[test]
pub(crate) fn test_compute_durations_empty_no_panic() {
let mut v: Vec<WordBoundary> = vec![];
compute_durations(&mut v);
}
#[test]
pub(crate) fn test_compute_durations_single_entry_floor_500ms() {
let mut v = vec![WordBoundary {
text: "Hi".into(),
offset: 0,
duration: 0,
estimated: false,
}];
compute_durations(&mut v);
assert_eq!(v[0].duration, 500);
}
#[test]
pub(crate) fn test_compute_distributions_fills_zero_from_next_offset() {
let mut v = vec![
WordBoundary {
text: "a".into(),
offset: 0,
duration: 0,
estimated: false,
},
WordBoundary {
text: "b".into(),
offset: 300,
duration: 0,
estimated: false,
},
WordBoundary {
text: "c".into(),
offset: 700,
duration: 0,
estimated: false,
},
];
compute_durations(&mut v);
assert_eq!(v[0].duration, 300); assert_eq!(v[1].duration, 400);
assert_eq!(v[2].duration, 500); }
#[test]
pub(crate) fn test_compute_durations_preserves_nonzero() {
let mut v = vec![
WordBoundary {
text: "a".into(),
offset: 0,
duration: 250,
estimated: false,
},
WordBoundary {
text: "b".into(),
offset: 250,
duration: 0,
estimated: false,
},
];
compute_durations(&mut v);
assert_eq!(v[0].duration, 250); }
#[test]
pub(crate) fn test_parse_elevenlabs_alignment_basic_words() {
let mut alignment = serde_json::Map::new();
alignment.insert(
"characters".into(),
serde_json::json!(["H", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]),
);
alignment.insert(
"character_start_times_seconds".into(),
serde_json::json!([0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50]),
);
alignment.insert(
"character_end_times_seconds".into(),
serde_json::json!([0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55]),
);
let words = parse_elevenlabs_alignment(&alignment);
assert_eq!(words.len(), 2);
assert_eq!(words[0].0, "Hello");
assert!((words[0].1 - 0.0).abs() < f32::EPSILON);
assert!((words[0].2 - 0.30).abs() < f32::EPSILON); assert_eq!(words[1].0, "world");
assert!((words[1].1 - 0.30).abs() < f32::EPSILON);
assert!((words[1].2 - 0.55).abs() < f32::EPSILON); }
#[test]
pub(crate) fn test_parse_elevenlabs_alignment_handles_mismatched_arrays() {
let mut alignment = serde_json::Map::new();
alignment.insert(
"characters".into(),
serde_json::json!(["H", "e", "l", "l", "o"]),
);
alignment.insert(
"character_start_times_seconds".into(),
serde_json::json!([0.0, 0.05]), );
alignment.insert(
"character_end_times_seconds".into(),
serde_json::json!([0.05, 0.10]), );
let words = parse_elevenlabs_alignment(&alignment);
assert_eq!(words.len(), 1);
assert_eq!(words[0].0, "Hello");
}
#[test]
pub(crate) fn test_parse_elevenlabs_alignment_missing_arrays_returns_empty() {
let alignment = serde_json::Map::new(); assert_eq!(parse_elevenlabs_alignment(&alignment).first(), None);
}
#[test]
pub(crate) fn test_looks_like_mp3_id3_tag() {
assert!(looks_like_mp3(b"ID3\x03\x00\x00\x00\x00"));
}
#[test]
pub(crate) fn test_looks_like_mp3_frame_sync() {
assert!(looks_like_mp3(&[0xFF, 0xE3, 0x10, 0x00]));
assert!(looks_like_mp3(&[0x00, 0x00, 0xFF, 0xFB, 0x90])); }
#[test]
pub(crate) fn test_looks_like_mp3_raw_pcm_is_false() {
assert!(!looks_like_mp3(&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05]));
assert!(!looks_like_mp3(&[]));
}
#[test]
pub(crate) fn test_decode_mp3_garbage_returns_empty_without_panicking() {
assert_eq!(decode_mp3_to_pcm16_mono(&[]).first(), None);
assert_eq!(
decode_mp3_to_pcm16_mono(b"definitely not mp3").first(),
None
);
}
#[test]
pub(crate) fn test_azure_ws_extract_path_basic() {
let frame = "X-RequestId:abc\r\nPath:turn.end\r\nContent-Type:application/json\r\n\r\n{}";
assert_eq!(azure_ws_extract_path(frame), "turn.end");
}
#[test]
pub(crate) fn test_azure_ws_extract_path_missing_returns_empty() {
let frame = "X-RequestId:abc\r\nContent-Type:application/json\r\n\r\n{}";
assert_eq!(azure_ws_extract_path(frame), "");
}
#[test]
pub(crate) fn test_azure_ws_extract_path_trims_whitespace() {
let frame = "Path: audio.metadata \r\n\r\n{}";
assert_eq!(azure_ws_extract_path(frame), "audio.metadata");
}
#[test]
pub(crate) fn test_azure_ws_extract_path_unicode_does_not_panic() {
let frame = "Path:tëst\r\n\r\n{}";
assert_eq!(azure_ws_extract_path(frame), "tëst");
}
#[test]
pub(crate) fn test_azure_ws_extract_body_crlf_separator() {
let frame = "X-RequestId:abc\r\nPath:response\r\n\r\n{\"Error\":{\"Message\":\"nope\"}}";
assert_eq!(
azure_ws_extract_body(frame),
"{\"Error\":{\"Message\":\"nope\"}}"
);
}
#[test]
pub(crate) fn test_azure_ws_extract_body_lf_separator() {
let frame = "X-RequestId:abc\nPath:response\n\n{}";
assert_eq!(azure_ws_extract_body(frame), "{}");
}
#[test]
pub(crate) fn test_azure_ws_extract_body_missing_returns_empty() {
assert_eq!(azure_ws_extract_body("just headers no body"), "");
}
#[test]
pub(crate) fn test_azure_ws_extract_error_message_field() {
let body = r#"{"Error":{"Message":"Authentication failed"}}"#;
assert_eq!(
azure_ws_extract_error(body).as_deref(),
Some("Authentication failed")
);
}
#[test]
pub(crate) fn test_azure_ws_extract_error_reason_fallback() {
let body = r#"{"Error":{}, "reason":"queued full"}"#;
assert_eq!(azure_ws_extract_error(body).as_deref(), Some("queued full"));
}
#[test]
pub(crate) fn test_azure_ws_extract_error_default_when_no_message() {
let body = r#"{"Error":{"Code":"SynthesisFailed"}}"#;
assert_eq!(
azure_ws_extract_error(body).as_deref(),
Some("Azure synthesis failed")
);
}
#[test]
pub(crate) fn test_azure_ws_extract_error_none_on_success_response() {
assert!(azure_ws_extract_error("{}").is_none());
assert!(azure_ws_extract_error(r#"{"foo":"bar"}"#).is_none());
}
#[test]
pub(crate) fn test_azure_ws_extract_error_invalid_json_returns_none() {
assert!(azure_ws_extract_error("not json").is_none());
}
#[test]
pub(crate) fn test_azure_ws_parse_word_boundary_current_shape() {
let item = serde_json::json!({
"Type": "WordBoundary",
"Data": {
"Offset": 500_000, "Duration": 2_500_000, "text": {"Text": "Hello"}
}
});
let (word, offset_ms, duration_ms, char_offset, char_len) =
azure_ws_parse_word_boundary(&item).expect("parsed");
assert_eq!(word, "Hello");
assert_eq!(offset_ms, 50);
assert_eq!(duration_ms, 250);
assert_eq!(char_offset, -1);
assert_eq!(char_len, -1);
}
#[test]
pub(crate) fn test_azure_ws_parse_word_boundary_with_text_offset() {
let item = serde_json::json!({
"Type": "WordBoundary",
"Data": {
"Offset": 2_800_000,
"Duration": 1_500_000,
"text": {"Text": "quick", "Offset": 4, "Length": 5}
}
});
let (word, _, _, char_offset, char_len) = azure_ws_parse_word_boundary(&item).expect("parsed");
assert_eq!(word, "quick");
assert_eq!(char_offset, 4);
assert_eq!(char_len, 5);
}
#[test]
pub(crate) fn test_azure_ws_parse_word_boundary_legacy_capital_t() {
let item = serde_json::json!({
"Type": "WordBoundary",
"Data": {
"Offset": 0,
"Duration": 100_000,
"Text": {"Text": "Hi"}
}
});
let (word, _, _, _, _) = azure_ws_parse_word_boundary(&item).expect("parsed");
assert_eq!(word, "Hi");
}
#[test]
pub(crate) fn test_azure_ws_parse_word_boundary_flat_string() {
let item = serde_json::json!({
"Type": "WordBoundary",
"Data": {"Offset": 0, "Duration": 0, "text": "Yo"}
});
let (word, _, _, _, _) = azure_ws_parse_word_boundary(&item).expect("parsed");
assert_eq!(word, "Yo");
}
#[test]
pub(crate) fn test_azure_ws_parse_word_boundary_filters_empty_text() {
let item = serde_json::json!({
"Type": "WordBoundary",
"Data": {"Offset": 0, "Duration": 0, "text": ""}
});
assert!(azure_ws_parse_word_boundary(&item).is_none());
}
#[test]
pub(crate) fn test_azure_ws_parse_word_boundary_missing_data() {
assert!(azure_ws_parse_word_boundary(&serde_json::json!({"Type": "WordBoundary"})).is_none());
}
#[test]
pub(crate) fn test_azure_ws_parse_viseme_basic() {
let item = serde_json::json!({
"Type": "Viseme",
"Data": {"VisemeId": 7, "Offset": 25_000_000} });
let (id, offset_sec) = azure_ws_parse_viseme(&item).expect("parsed");
assert_eq!(id, 7);
assert!((offset_sec - 2.5).abs() < 0.01);
}
#[test]
pub(crate) fn test_azure_ws_parse_viseme_missing_data() {
assert!(azure_ws_parse_viseme(&serde_json::json!({"Type": "Viseme"})).is_none());
}
#[test]
pub(crate) fn test_streaming_chunk_size_constant_value() {
assert_eq!(STREAMING_CHUNK_SIZE, 8 * 1024);
assert!(STREAMING_CHUNK_SIZE.is_power_of_two());
}
#[test]
pub(crate) fn test_streaming_chunk_size_used_in_speak_path() {
let mut source = String::new();
for f in [
"edge.rs",
"mod.rs",
"engine.rs",
"config.rs",
"decode.rs",
"ssml.rs",
"google.rs",
"gemini.rs",
"elevenlabs.rs",
"voices.rs",
] {
let path = std::path::Path::new("src/cloud_engine").join(f);
source.push_str(
&std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())),
);
}
let production_uses = source
.lines()
.filter(|l| !l.contains("test_streaming_chunk_size_used_in_speak_path"))
.filter(|l| !l.contains("magic 8192"))
.filter(|l| l.contains(".chunks(STREAMING_CHUNK_SIZE)"))
.count();
assert!(
production_uses >= 2,
"expected at least 2 production uses of STREAMING_CHUNK_SIZE, found {production_uses}"
);
}
#[test]
pub(crate) fn test_speechmarkdown_routing_is_per_platform() {
use crate::engine::preprocess_speech_markdown;
let probe_inputs = [
"(world)[emphasis:\"strong\"]",
"+important+",
"(world)[rate:\"fast\"]",
"(world)[pitch:\"high\"]",
"(world)[volume:\"loud\"]",
"[rate:\"fast\"]hello[/rate]",
"This is ^italic^ text",
];
let mut found_distinct = false;
for input in &probe_inputs {
let (azure_ssml, azure_ok) = preprocess_speech_markdown(input, "azure");
let (google_ssml, google_ok) = preprocess_speech_markdown(input, "google");
let (alexa_ssml, alexa_ok) = preprocess_speech_markdown(input, "openai");
assert!(azure_ok, "azure failed to parse: {input:?}");
assert!(google_ok, "google failed to parse: {input:?}");
assert!(alexa_ok, "alexa failed to parse: {input:?}");
if azure_ssml != google_ssml || google_ssml != alexa_ssml {
found_distinct = true;
break;
}
}
assert!(
found_distinct,
"None of {} probe inputs produced distinct SSML across azure/google/alexa. \
This either means the speechmarkdown-rust library has collapsed its \
Platform variants to identical output (check the dependency version) or \
the routing match arm in preprocess_speech_markdown is broken.",
probe_inputs.len()
);
}
#[test]
pub(crate) fn test_speechmarkdown_elevenlabs_dialects() {
use crate::engine::preprocess_speech_markdown;
let (out, is_ssml) = preprocess_speech_markdown("Hello [2s] world", "elevenlabs");
assert!(!is_ssml, "elevenlabs dialect must not be flagged as SSML");
assert_eq!(out, "Hello <break time=\"2s\"/> world");
let (out, is_ssml) = preprocess_speech_markdown("Hello [2s] world", "elevenlabs-v3");
assert!(!is_ssml);
assert_eq!(out, "Hello [long pause] world");
let (out, _) = preprocess_speech_markdown("(secret)[whisper]", "elevenlabs-v3");
assert_eq!(out, "[whispers] secret");
let (out, _) = preprocess_speech_markdown("(speech)/spitʃ/", "elevenlabs-v3");
assert_eq!(out, "\"/spitʃ/\"");
}
#[test]
pub(crate) fn test_speechmarkdown_other_providers_detect_input() {
use crate::engine::preprocess_speech_markdown;
for provider in ["openai", "cartesia", "murf", "deepgram", "witai", "xai"] {
let (_ssml, is_ssml) =
preprocess_speech_markdown("Hello (world)[emphasis:\"strong\"]", provider);
assert!(
is_ssml,
"provider '{provider}' should detect SpeechMarkdown"
);
}
}
#[test]
pub(crate) fn test_speechmarkdown_plain_text_passes_through_unprocessed() {
use crate::engine::preprocess_speech_markdown;
for provider in ["azure", "google", "openai", "elevenlabs"] {
let (out, is_ssml) = preprocess_speech_markdown("Just a plain sentence.", provider);
assert!(!is_ssml, "provider '{provider}' flagged plain text as SSML");
assert_eq!(out, "Just a plain sentence.");
}
}
#[test]
pub(crate) fn test_elevenlabs_synth_url_gains_with_timestamps_when_boundary_requested() {
let cfg = build_config("elevenlabs", &engine_creds("elevenlabs")).unwrap();
let mut url = cfg.synth_url.clone();
url.push_str("/with-timestamps");
assert!(url.ends_with("/text-to-speech/21m00Tcm4TlvDq8ikWAM/with-timestamps"));
}
#[test]
pub(crate) fn test_elevenlabs_model_id_from_creds() {
let cfg = build_config("elevenlabs", &engine_creds("elevenlabs")).unwrap();
assert_eq!(cfg.model_default.as_deref(), Some("eleven_v4"));
assert_eq!(
elevenlabs_smd_platform("elevenlabs", cfg.model_default.as_deref()),
"elevenlabs-v3"
);
let mut c = engine_creds("elevenlabs");
c.insert("modelId".into(), "eleven_multilingual_v2".into());
let cfg = build_config("elevenlabs", &c).unwrap();
assert_eq!(cfg.model_default.as_deref(), Some("eleven_multilingual_v2"));
assert_eq!(
elevenlabs_smd_platform("elevenlabs", cfg.model_default.as_deref()),
"elevenlabs"
);
let mut c = engine_creds("elevenlabs");
c.insert("modelId".into(), "eleven_flash_v2_5".into());
let cfg = build_config("elevenlabs", &c).unwrap();
assert_eq!(cfg.model_default.as_deref(), Some("eleven_flash_v2_5"));
let mut c = engine_creds("elevenlabs");
c.insert("modelId".into(), String::new());
let cfg = build_config("elevenlabs", &c).unwrap();
assert_eq!(cfg.model_default.as_deref(), Some("eleven_v4"));
}
#[test]
pub(crate) fn test_elevenlabs_ssml_translates_to_dialects() {
let alexa = r#"<speak>Hello <break time="2s"/> world</speak>"#;
assert_eq!(
ssml_to_dialect(alexa, "elevenlabs").unwrap(),
"Hello <break time=\"2s\"/> world"
);
let v3 = r#"<speak><amazon:effect name="whispered">secret</amazon:effect></speak>"#;
assert_eq!(
ssml_to_dialect(v3, "elevenlabs-v3").unwrap(),
"[whispers] secret"
);
let azure = r#"<speak><mstts:express-as style="cheerful">hi</mstts:express-as></speak>"#;
assert_eq!(
ssml_to_dialect(azure, "elevenlabs-v3").unwrap(),
"[cheerful] hi"
);
assert_eq!(
ssml_to_dialect("no ssml here", "elevenlabs").unwrap(),
"no ssml here"
);
assert!(ssml_to_dialect("<speak>a & b</speak>", "elevenlabs").is_none());
let voiced = r#"<speak><voice name="Aria">hi</voice></speak>"#;
assert_eq!(ssml_to_dialect(voiced, "elevenlabs-v3").unwrap(), "hi");
}
#[test]
pub(crate) fn test_elevenlabs_dialect_follows_extra_body_model_id() {
let mut cfg = build_config("elevenlabs", &engine_creds("elevenlabs")).unwrap();
assert_eq!(cfg.model_default.as_deref(), Some("eleven_v4"));
cfg.extra_body.insert(
"model_id".to_string(),
serde_json::json!("eleven_multilingual_v2"),
);
let effective = effective_model(&cfg);
assert_eq!(
elevenlabs_smd_platform("elevenlabs", effective),
"elevenlabs",
"pre-v3 override must select the <break> dialect"
);
}
#[test]
pub(crate) fn test_elevenlabs_dialect_follows_model() {
assert_eq!(
elevenlabs_smd_platform("elevenlabs", Some("eleven_v3")),
"elevenlabs-v3"
);
assert_eq!(
elevenlabs_smd_platform("elevenlabs", Some("eleven_v3_conversational")),
"elevenlabs-v3"
);
assert_eq!(
elevenlabs_smd_platform("elevenlabs", Some("eleven_multilingual_v2")),
"elevenlabs"
);
assert_eq!(
elevenlabs_smd_platform("elevenlabs", Some("eleven_flash_v2")),
"elevenlabs"
);
assert_eq!(elevenlabs_smd_platform("azure", Some("eleven_v3")), "azure");
assert_eq!(elevenlabs_smd_platform("elevenlabs", None), "elevenlabs");
}
#[test]
pub(crate) fn test_elevenlabs_v4_shares_the_audio_tag_dialect() {
assert_eq!(
elevenlabs_smd_platform("elevenlabs", Some("eleven_v4")),
"elevenlabs-v3"
);
assert_eq!(
elevenlabs_smd_platform("elevenlabs", Some("eleven_v4_turbo")),
"elevenlabs-v3"
);
}
#[test]
pub(crate) fn test_elevenlabs_ssml_phoneme_round_trips_to_native_ipa() {
let out = ssml_to_dialect(
"<speak>city of <phoneme alphabet=\"ipa\" ph=\"ˌsænfrənˈsɪskoʊ\">San Francisco</phoneme></speak>",
"elevenlabs-v3",
)
.expect("dialect conversion");
assert_eq!(out, "city of \"/ˌsænfrənˈsɪskoʊ/\"");
let out = ssml_to_dialect(
"<speak><phoneme alphabet=\"cmu-arpabet\" ph=\"M AE1 D\">Madison</phoneme></speak>",
"elevenlabs-v3",
)
.expect("dialect conversion");
assert!(!out.contains("phoneme"), "{out}");
}
#[test]
pub(crate) fn test_auth_value_openai_bearer_scheme() {
let cfg = build_config("openai", &engine_creds("openai")).unwrap();
let value = format!("{}{}", cfg.auth_prefix, "TESTKEY");
assert_eq!(value, "Bearer TESTKEY");
}
#[test]
pub(crate) fn test_auth_value_azure_raw_key() {
let cfg = build_config("azure", &engine_creds("azure")).unwrap();
let value = format!("{}{}", cfg.auth_prefix, "TESTKEY");
assert_eq!(value, "TESTKEY");
}
#[test]
pub(crate) fn test_auth_value_deepgram_token_scheme() {
let cfg = build_config("deepgram", &engine_creds("deepgram")).unwrap();
let value = format!("{}{}", cfg.auth_prefix, "TESTKEY");
assert_eq!(value, "Token TESTKEY");
}
#[test]
pub(crate) fn test_auth_value_resemble_token_scheme() {
let cfg = build_config("resemble", &engine_creds("resemble")).unwrap();
let value = format!("{}{}", cfg.auth_prefix, "TESTKEY");
assert_eq!(value, "Token TESTKEY");
}
#[test]
pub(crate) fn test_auth_value_elevenlabs_no_prefix() {
let cfg = build_config("elevenlabs", &engine_creds("elevenlabs")).unwrap();
let value = format!("{}{}", cfg.auth_prefix, "TESTKEY");
assert_eq!(value, "TESTKEY");
}
#[test]
pub(crate) fn test_auth_value_modelslab_no_header_sent() {
let cfg = build_config("modelslab", &engine_creds("modelslab")).unwrap();
assert_eq!(cfg.auth_header, "");
}
#[test]
pub(crate) fn test_gemini_config_defaults() {
let cfg = build_config("gemini", &engine_creds("gemini")).unwrap();
assert_eq!(cfg.provider_id, "gemini");
assert_eq!(cfg.auth_header, "x-goog-api-key");
assert_eq!(cfg.auth_prefix, "");
assert_eq!(cfg.model_default.as_deref(), Some("gemini-3.8-flash-tts"));
assert_eq!(cfg.default_voice.as_deref(), Some("Kore"));
assert!(!cfg.body_is_ssml);
assert!(cfg.voices_url.is_some());
assert!(cfg.synth_url.contains("/v1beta/interactions"));
}
#[test]
pub(crate) fn test_gemini_config_credential_overrides() {
let mut creds = engine_creds("gemini");
creds.insert("modelId".into(), "gemini-3.8-flash-lite-tts".into());
creds.insert("voice".into(), "Puck".into());
let cfg = build_config("gemini", &creds).unwrap();
assert_eq!(
cfg.model_default.as_deref(),
Some("gemini-3.8-flash-lite-tts")
);
assert_eq!(cfg.default_voice.as_deref(), Some("Puck"));
}
#[test]
pub(crate) fn test_gemini_request_minimal_body() {
let body = build_gemini_request(
"Have a wonderful day!",
"Kore",
0.0,
0.0,
0.0,
Some("gemini-3.8-flash-tts"),
None,
);
assert_eq!(body["model"], "gemini-3.8-flash-tts");
assert_eq!(body["response_format"]["type"], "audio");
assert_eq!(
body["generation_config"]["speech_config"][0]["voice"],
"Kore"
);
let content = &body["input"][0]["content"][0];
assert_eq!(content["type"], "text");
assert_eq!(content["text"], "Have a wonderful day!");
assert!(content.get("annotations").is_none());
}
#[test]
pub(crate) fn test_gemini_request_style_from_params() {
let body = build_gemini_request(
"Slow down.",
"Kore",
0.7, 1.2, 0.0,
None,
None,
);
let content = &body["input"][0]["content"][0];
let annotations = content["annotations"].as_array().unwrap();
assert_eq!(annotations[0]["type"], "speech_metadata");
let style = annotations[0]["style"].as_str().unwrap();
assert!(style.contains("speaking slowly"), "style: {style}");
assert!(style.contains("high pitch"), "style: {style}");
}
#[test]
pub(crate) fn test_gemini_request_style_override_wins() {
let body = build_gemini_request(
"Hello",
"Kore",
0.5,
0.0,
0.0,
None,
Some("whispered urgently"),
);
let style = body["input"][0]["content"][0]["annotations"][0]["style"]
.as_str()
.unwrap();
assert_eq!(style, "whispered urgently");
}
#[test]
pub(crate) fn test_gemini_request_default_model() {
let body = build_gemini_request("Hi", "Kore", 0.0, 0.0, 0.0, None, None);
assert_eq!(body["model"], "gemini-3.8-flash-tts");
}
#[test]
pub(crate) fn test_gemini_interaction_audio_parse() {
let wav = b"RIFFxxxxWAVEfmt";
let b64 = {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(wav)
};
let json = serde_json::json!({
"steps": [{
"type": "model_output",
"content": [
{ "type": "text", "text": "partial" },
{ "type": "audio", "mime_type": "audio/wav", "data": b64 }
]
}]
});
assert_eq!(
parse_gemini_interaction_audio(&json),
GeminiAudioBlock::Present(wav.to_vec())
);
}
#[test]
pub(crate) fn test_gemini_interaction_audio_corrupt_base64() {
let json = serde_json::json!({
"steps": [{ "content": [
{ "type": "audio", "mime_type": "audio/wav", "data": "!!!not base64!!!" }
]}]
});
assert_eq!(
parse_gemini_interaction_audio(&json),
GeminiAudioBlock::Corrupt
);
}
#[test]
pub(crate) fn test_gemini_interaction_audio_none_when_absent() {
assert_eq!(
parse_gemini_interaction_audio(&serde_json::json!({})),
GeminiAudioBlock::Absent
);
assert_eq!(
parse_gemini_interaction_audio(&serde_json::json!({
"steps": [{ "type": "model_output", "content": [] }]
})),
GeminiAudioBlock::Absent
);
assert_eq!(
parse_gemini_interaction_audio(&serde_json::json!({
"error": { "code": 400, "message": "bad" }
})),
GeminiAudioBlock::Absent
);
}
#[test]
pub(crate) fn test_gemini_voices_mapping() {
let arr = serde_json::json!([
{
"id": "kore",
"display_name": "Kore",
"language_code": "en-US",
"accent": "American",
"persona": "Firm",
"gender": "female"
},
{
"id": "voice_abc123",
"display_name": "My Designed Voice",
"language_code": "de-DE"
}
]);
let voices = map_gemini_voices(arr.as_array().unwrap());
assert_eq!(voices.len(), 2);
assert_eq!(voices[0].id, "kore");
assert_eq!(voices[0].provider, "gemini");
assert!(voices[0].name.contains("Kore"));
assert!(voices[0].name.contains("Firm"), "persona in display name");
assert_eq!(voices[0].language_codes[0].bcp47, "en-US");
assert_eq!(voices[1].name, "My Designed Voice");
assert_eq!(voices[1].language_codes[0].bcp47, "de-DE");
}
#[test]
pub(crate) fn test_gemini_style_params_neutral_is_empty() {
assert_eq!(gemini_style_from_params(1.0, 1.0, 1.0), "");
assert_eq!(gemini_style_from_params(0.0, 0.0, 0.0), "");
}
#[test]
pub(crate) fn test_speechmarkdown_gemini_dialect_routing() {
let (out, is_ssml) = preprocess_speech_markdown("Wait [500ms] then [laugh] loudly", "gemini");
assert!(!is_ssml, "gemini dialect is prompt text, not SSML");
assert!(out.contains("<short pause>"), "out: {out}");
assert!(out.contains("<laugh>"), "out: {out}");
assert!(!out.contains("<speak>"), "no SSML envelope: {out}");
}
#[test]
pub(crate) fn test_gemini_ssml_input_translated_to_dialect() {
#[cfg(feature = "speechmarkdown")]
{
let dialect = ssml_to_dialect(
"<speak>Hello <break time=\"500ms\"/> world</speak>",
"gemini",
);
let out = dialect.expect("SSML → gemini dialect");
assert!(out.contains("Hello"), "out: {out}");
assert!(out.contains("<short pause>"), "break preserved: {out}");
}
}
#[test]
pub(crate) fn test_gemini_engine_id_and_listing() {
let engine = CloudEngine::new("gemini", &engine_creds("gemini")).unwrap();
assert_eq!(engine.engine_id(), "gemini");
let listed = crate::factory::engine_list();
assert!(listed.iter().any(|e| e.id == "gemini"), "engine listed");
}
#[test]
pub(crate) fn test_gemini_dialect_selector() {
assert_eq!(
elevenlabs_smd_platform("gemini", Some("gemini-3.8-flash-tts")),
"gemini"
);
assert_eq!(
elevenlabs_smd_platform("gemini", Some("gemini-3.8-flash-lite-tts")),
"gemini"
);
}
pub(crate) fn tiny_wav(samples: &[i16], sample_rate: u32) -> Vec<u8> {
let data_len = samples.len() * 2;
let mut wav = Vec::with_capacity(44 + data_len);
wav.extend_from_slice(b"RIFF");
wav.extend_from_slice(&(36 + data_len as u32).to_le_bytes());
wav.extend_from_slice(b"WAVE");
wav.extend_from_slice(b"fmt ");
wav.extend_from_slice(&16u32.to_le_bytes());
wav.extend_from_slice(&1u16.to_le_bytes()); wav.extend_from_slice(&1u16.to_le_bytes()); wav.extend_from_slice(&sample_rate.to_le_bytes());
wav.extend_from_slice(&(sample_rate * 2).to_le_bytes());
wav.extend_from_slice(&2u16.to_le_bytes());
wav.extend_from_slice(&16u16.to_le_bytes());
wav.extend_from_slice(b"data");
wav.extend_from_slice(&(data_len as u32).to_le_bytes());
for s in samples {
wav.extend_from_slice(&s.to_le_bytes());
}
wav
}
#[test]
pub(crate) fn test_gemini_wav_decode_roundtrip() {
let samples: Vec<i16> = (0..480).map(|i| (i * 37 % 3000) as i16).collect();
let wav = tiny_wav(&samples, 24_000);
let pcm = decode_audio_to_pcm16_mono(&wav, "wav");
assert_eq!(pcm.len(), samples.len() * 2, "16-bit mono out");
for (i, s) in samples.iter().enumerate() {
let out = i16::from_le_bytes([pcm[i * 2], pcm[i * 2 + 1]]);
assert!((out - s).abs() <= 2, "sample {i}: {out} vs {s}");
}
}
#[test]
pub(crate) fn test_wav_sample_rate_parsing() {
let samples = [0i16; 16];
assert_eq!(wav_sample_rate(&tiny_wav(&samples, 24_000)), 24_000);
assert_eq!(wav_sample_rate(&tiny_wav(&samples, 44_100)), 44_100);
assert_eq!(wav_sample_rate(b"not a wav file at all......"), 24_000);
assert_eq!(wav_sample_rate(&[]), 24_000);
}
type BoundaryEvent = (String, f32, f32, i32, i32, bool);
#[test]
pub(crate) fn test_fire_scaled_estimates_scales_to_duration() {
let pcm = vec![0u8; 2 * 24_000 * 2];
let mut events: Vec<BoundaryEvent> = Vec::new();
{
let mut cb: crate::engine::OnBoundaryCallback<'_> =
&mut |word: &str, start: f32, end: f32, offset: i32, len: i32, est: bool| {
events.push((word.to_string(), start, end, offset, len, est));
};
fire_scaled_estimates(&mut cb, "one two three four five six seven", &pcm, 24_000);
}
assert!(!events.is_empty(), "events fired");
let last_end = events.last().unwrap().2;
assert!(
(last_end - 2.0).abs() < 0.2,
"last end {last_end} scaled to ~2.0 s of audio"
);
assert!(events.iter().all(|e| e.5), "estimated flag set");
assert!(
events.iter().all(|e| e.0.split(' ').count() == 1),
"one word per event"
);
}
#[test]
pub(crate) fn test_fire_scaled_estimates_empty_pcm() {
let mut fired = 0;
let mut cb: crate::engine::OnBoundaryCallback<'_> =
&mut |_w: &str, _s: f32, _e: f32, _o: i32, _l: i32, _est: bool| {
fired += 1;
};
fire_scaled_estimates(&mut cb, "some words here", &[], 24_000);
assert_eq!(fired, 0, "zero-length audio fires nothing (all at t=0)");
}