speechmarkdown_rust/formatters/ssml/
google_assistant.rs1use 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 §ion_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(§ion_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(§ion_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}