rust_tts_wrapper/cloud_engine/
google.rs1#![allow(clippy::wildcard_imports)]
3
4use crate::types::WordBoundary;
5
6#[must_use]
13pub fn build_google_request(
14 text: &str,
15 voice: &str,
16 add_marks: bool,
17 input_ssml: Option<&str>,
18) -> (serde_json::Value, Vec<String>) {
19 let voice_obj = if voice.len() >= 5 && voice.as_bytes().get(2) == Some(&b'-') {
24 let lang = &voice[..5];
25 serde_json::json!({ "languageCode": lang, "name": voice })
26 } else {
27 serde_json::json!({ "name": voice })
28 };
29
30 let mut words_list = Vec::new();
31
32 let input = if let Some(ssml) = input_ssml {
33 serde_json::json!({ "ssml": ssml })
34 } else if add_marks {
35 let words: Vec<&str> = text.split_whitespace().filter(|w| !w.is_empty()).collect();
36 let mut ssml = String::from("<speak>");
37 for (i, w) in words.iter().enumerate() {
38 if i > 0 {
39 ssml.push(' ');
40 }
41 let _ = std::fmt::Write::write_fmt(&mut ssml, format_args!("<mark name=\"{i}\"/>{w}"));
42 words_list = words.iter().map(|w| (*w).to_string()).collect();
43 }
44 ssml.push_str("</speak>");
45 serde_json::json!({ "ssml": ssml })
46 } else {
47 serde_json::json!({ "text": text })
48 };
49
50 let mut body = serde_json::json!({
51 "input": input,
52 "voice": voice_obj,
53 "audioConfig": { "audioEncoding": "MP3", "sampleRateHertz": 24000 }
57 });
58
59 if add_marks {
60 body["enableTimePointing"] = serde_json::json!(["SSML_MARK"]);
61 }
62
63 (body, words_list)
64}
65
66pub(crate) fn parse_google_timepoints(
68 timepoints: &[serde_json::Value],
69 words: &[String],
70) -> Vec<WordBoundary> {
71 #[derive(Clone)]
72 struct RawTp {
73 index: usize,
74 time_ms: u64,
75 }
76
77 let mut raw: Vec<RawTp> = Vec::new();
78 for tp in timepoints {
79 let mark = tp.get("markName").and_then(|v| v.as_str()).unwrap_or("");
80 let idx: usize = mark.parse().unwrap_or(usize::MAX);
81 let secs = tp
82 .get("timeSeconds")
83 .and_then(serde_json::Value::as_f64)
84 .unwrap_or(0.0);
85 if idx < words.len() {
86 raw.push(RawTp {
87 index: idx,
88 time_ms: (secs * 1000.0) as u64,
89 });
90 }
91 }
92 raw.sort_by_key(|r| r.time_ms);
93
94 let mut boundaries = Vec::with_capacity(raw.len());
95 for (i, tp) in raw.iter().enumerate() {
96 let word = &words[tp.index];
97 let duration = if i + 1 < raw.len() {
98 raw[i + 1].time_ms.saturating_sub(tp.time_ms)
99 } else {
100 ((word.len() as u64) * 80).max(50)
101 };
102 boundaries.push(WordBoundary {
103 text: word.clone(),
104 offset: tp.time_ms,
105 duration,
106 estimated: false,
107 });
108 }
109 boundaries
110}