Skip to main content

speechmarkdown_rust/formatters/ssml/
google_assistant.rs

1use crate::ast::{AstNode, NodeType};
2use crate::error::Result;
3use crate::formatters::base::{Formatter, FormatterOptions};
4use crate::formatters::ssml::base::{
5    attrs_merge, format_attr_string_ordered, SsmlFormatterBase, TagAttrs, TagInfo,
6};
7
8pub struct GoogleAssistantSsmlFormatter {
9    base: SsmlFormatterBase,
10    options: FormatterOptions,
11}
12
13impl GoogleAssistantSsmlFormatter {
14    pub fn new(options: FormatterOptions) -> Self {
15        let base = SsmlFormatterBase::new(options.clone());
16        Self { base, options }
17    }
18
19    fn google_attribute_to_tag(&self, key: &str, value: &str) -> Option<TagInfo> {
20        let mut attributes: TagAttrs = Vec::new();
21        match key.to_lowercase().as_str() {
22            "whisper" => {
23                attributes.push(("volume".to_string(), "x-soft".to_string()));
24                attributes.push(("rate".to_string(), "slow".to_string()));
25                Some(("prosody".to_string(), attributes))
26            }
27            "excited" | "disappointed" => None,
28            "voice" | "lang" => None,
29            "ipa" => None,
30            "style" => {
31                if !value.is_empty() {
32                    attributes.push(("name".to_string(), value.to_string()));
33                }
34                Some(("google:style".to_string(), attributes))
35            }
36            _ => self.base.attribute_to_tag(key, value),
37        }
38    }
39
40    fn format_google_text_modifier(&self, node: &AstNode) -> Result<String> {
41        let mut tags: Vec<TagInfo> = Vec::new();
42        let mut last_say_as: Option<TagInfo> = None;
43        let mut has_ipa = false;
44        let mut non_ipa_count = 0;
45
46        for key in &node.attribute_keys {
47            let value = match node.attributes.get(key) {
48                Some(v) => v,
49                None => continue,
50            };
51
52            if key.to_lowercase() == "ipa" {
53                has_ipa = true;
54            } else {
55                non_ipa_count += 1;
56            }
57
58            if let Some(tag_info) = self.google_attribute_to_tag(key, value) {
59                let tag_name = tag_info.0.clone();
60                if tag_name == "prosody" {
61                    if let Some(existing) = tags.iter_mut().find(|(name, _)| name == "prosody") {
62                        attrs_merge(&mut existing.1, tag_info.1);
63                        continue;
64                    }
65                }
66                if tag_name == "say-as" {
67                    last_say_as = Some(tag_info);
68                    continue;
69                }
70                tags.push(tag_info);
71            }
72        }
73
74        if has_ipa && non_ipa_count == 0 {
75            return Ok(node.text.clone());
76        }
77
78        if has_ipa {
79            if let Some(ipa_tag) = self
80                .base
81                .attribute_to_tag("ipa", node.attributes.get("ipa").unwrap_or(&String::new()))
82            {
83                tags.push(ipa_tag);
84            }
85        }
86
87        if let Some(say_as) = last_say_as {
88            tags.push(say_as);
89        }
90
91        if tags.is_empty() {
92            return Ok(node.text.clone());
93        }
94
95        self.base.apply_tags_to_text(&node.text, &tags)
96    }
97
98    fn format_google_section(&self, node: &AstNode) -> Result<String> {
99        let mut tags: Vec<TagInfo> = Vec::new();
100
101        for key in &node.attribute_keys {
102            let value = match node.attributes.get(key) {
103                Some(v) => v,
104                None => continue,
105            };
106            if let Some(tag_info) = self.google_attribute_to_tag(key, value) {
107                let tag_name = tag_info.0.clone();
108                if tag_name == "prosody" {
109                    if let Some(existing) = tags.iter_mut().find(|(name, _)| name == "prosody") {
110                        attrs_merge(&mut existing.1, tag_info.1);
111                        continue;
112                    }
113                }
114                tags.push(tag_info);
115            }
116        }
117
118        if tags.is_empty() {
119            return Ok(String::new());
120        }
121
122        let section_tag_order = ["voice", "lang", "prosody", "emphasis"];
123        tags.sort_by_key(|(tag_name, _)| {
124            section_tag_order
125                .iter()
126                .position(|t| t == tag_name)
127                .unwrap_or(usize::MAX)
128        });
129
130        let mut result = String::new();
131        for (i, (tag_name, attrs)) in tags.iter().enumerate() {
132            let attr_string = format_attr_string_ordered(tag_name, attrs);
133            if i > 0 {
134                result.push('\n');
135            }
136            if attr_string.is_empty() {
137                result.push_str(&format!("<{}>", tag_name));
138            } else {
139                result.push_str(&format!("<{} {}>", tag_name, attr_string));
140            }
141        }
142        Ok(result)
143    }
144
145    fn format_google_section_close(&self, node: &AstNode) -> Result<String> {
146        let mut tags: Vec<TagInfo> = Vec::new();
147
148        for key in &node.attribute_keys {
149            let value = match node.attributes.get(key) {
150                Some(v) => v,
151                None => continue,
152            };
153            if let Some(tag_info) = self.google_attribute_to_tag(key, value) {
154                let tag_name = tag_info.0.clone();
155                if tag_name == "prosody" {
156                    if let Some(existing) = tags.iter_mut().find(|(name, _)| name == "prosody") {
157                        attrs_merge(&mut existing.1, tag_info.1);
158                        continue;
159                    }
160                }
161                tags.push(tag_info);
162            }
163        }
164
165        let section_tag_order = ["voice", "lang", "prosody", "emphasis"];
166        tags.sort_by_key(|(tag_name, _)| {
167            section_tag_order
168                .iter()
169                .position(|t| t == tag_name)
170                .unwrap_or(usize::MAX)
171        });
172
173        if tags.is_empty() {
174            return Ok(String::new());
175        }
176
177        let mut result = String::new();
178        for (i, (tag_name, _)) in tags.iter().rev().enumerate() {
179            result.push_str(&format!("</{}>", tag_name));
180            if i < tags.len() - 1 {
181                result.push('\n');
182            }
183        }
184        Ok(result)
185    }
186}
187
188impl Formatter for GoogleAssistantSsmlFormatter {
189    fn format(&self, ast: &AstNode) -> Result<String> {
190        let mut content = String::new();
191        let mut children_iter = ast.children.iter().peekable();
192
193        while let Some(child) = children_iter.next() {
194            if child.node_type == NodeType::Section {
195                let mut section_content_raw = String::new();
196                while let Some(next_child) = children_iter.peek() {
197                    if next_child.node_type == NodeType::Section {
198                        break;
199                    }
200                    let next_child = children_iter.next().unwrap();
201                    section_content_raw.push_str(&self.format_google_node(next_child)?);
202                }
203                let section_content = if let Some(stripped) = section_content_raw.strip_prefix('\n')
204                {
205                    stripped
206                } else {
207                    &section_content_raw
208                };
209
210                let section_open = self.format_google_section(child)?;
211                let section_close = if !section_open.is_empty() {
212                    self.format_google_section_close(child)?
213                } else {
214                    String::new()
215                };
216
217                content.push_str(&section_open);
218                if !section_open.is_empty() && section_content.starts_with('\n') {
219                    content.push('\n');
220                }
221                let final_content = if section_open.is_empty() {
222                    section_content.trim_start()
223                } else {
224                    section_content
225                };
226                content.push_str(final_content);
227                content.push_str(&section_close);
228            } else {
229                content.push_str(&self.format_google_node(child)?);
230            }
231        }
232
233        if self.options.include_speak_tag {
234            let content = content.trim_end_matches('\n');
235            Ok(format!("<speak>\n{}\n</speak>", content))
236        } else {
237            Ok(content)
238        }
239    }
240
241    fn format_node(&self, node: &AstNode) -> Result<String> {
242        self.format_google_node(node)
243    }
244}
245
246impl GoogleAssistantSsmlFormatter {
247    fn format_google_node(&self, node: &AstNode) -> Result<String> {
248        match node.node_type {
249            NodeType::PlainText => Ok(node.text.clone()),
250            NodeType::TextModifier => self.format_google_text_modifier(node),
251            _ => self.base.format_node_internal(node),
252        }
253    }
254}