Skip to main content

speechmarkdown_rust/formatters/ssml/
amazon_alexa.rs

1use crate::ast::{AstNode, NodeType};
2use crate::error::Result;
3use crate::formatters::base::{Formatter, FormatterOptions};
4use crate::formatters::ssml::base::SsmlFormatterBase;
5
6pub struct AmazonAlexaSsmlFormatter {
7    base: SsmlFormatterBase,
8}
9
10impl AmazonAlexaSsmlFormatter {
11    pub fn new(options: FormatterOptions) -> Self {
12        let base = SsmlFormatterBase::new(options);
13
14        Self { base }
15    }
16
17    /// Override to add Amazon-specific functionality
18    fn format_amazon_audio(&self, node: &AstNode) -> Result<String> {
19        let src = node.attributes.get("src").unwrap_or(&String::new()).clone();
20
21        let caption = &node.text;
22
23        if caption.is_empty() {
24            Ok(format!("<audio src=\"{}\"/>", src))
25        } else {
26            Ok(format!(
27                "<audio src=\"{}\">\n<desc>{}</desc>\n</audio>",
28                src,
29                self.base.escape_xml(caption)
30            ))
31        }
32    }
33
34    /// Format Amazon-specific emotion tags
35    fn format_amazon_emotion(&self, node: &AstNode) -> Result<String> {
36        let emotion_type = match node.node_type {
37            NodeType::Excited => "excited",
38            NodeType::Disappointed => "disappointed",
39            _ => return Ok(String::new()),
40        };
41
42        let intensity = node
43            .attributes
44            .get("value")
45            .unwrap_or(&"medium".to_string())
46            .clone();
47
48        Ok(format!(
49            "<amazon:emotion name=\"{}\" intensity=\"{}\">",
50            emotion_type, intensity
51        ))
52    }
53
54    /// Format Amazon-specific domain tags
55    fn format_amazon_domain(&self, node: &AstNode) -> Result<String> {
56        let domain_name = match node.node_type {
57            NodeType::Newscaster => "news",
58            NodeType::Dj => "music",
59            _ => return Ok(String::new()),
60        };
61
62        Ok(format!("<amazon:domain name=\"{}\">", domain_name))
63    }
64}
65
66impl Formatter for AmazonAlexaSsmlFormatter {
67    fn format(&self, ast: &AstNode) -> Result<String> {
68        self.base.format(ast)
69    }
70
71    fn format_node(&self, node: &AstNode) -> Result<String> {
72        match node.node_type {
73            // Audio elements
74            NodeType::Audio => self.format_amazon_audio(node),
75
76            // Amazon-specific emotions
77            NodeType::Excited | NodeType::Disappointed => self.format_amazon_emotion(node),
78
79            // Amazon-specific domains
80            NodeType::Newscaster | NodeType::Dj => self.format_amazon_domain(node),
81
82            // Use base formatter for everything else
83            _ => self.base.format_node(node),
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use crate::parser::SpeechMarkdownParser;
91
92    #[test]
93    fn test_amazon_alexa_basic_parsing() {
94        let input = "Hello world";
95        let result =
96            SpeechMarkdownParser::to_ssml(input, crate::formatters::base::Platform::AmazonAlexa);
97        assert!(result.is_ok());
98    }
99
100    #[test]
101    fn test_amazon_alexa_with_break() {
102        let input = "Sample [2s] text";
103        let result =
104            SpeechMarkdownParser::to_ssml(input, crate::formatters::base::Platform::AmazonAlexa);
105        assert!(result.is_ok());
106
107        let ssml = result.unwrap();
108        assert!(ssml.contains("<break time=\"2s\"/>"));
109    }
110
111    #[test]
112    fn test_amazon_alexa_with_emphasis() {
113        let input = "++strong emphasis++";
114        let result =
115            SpeechMarkdownParser::to_ssml(input, crate::formatters::base::Platform::AmazonAlexa);
116        assert!(result.is_ok());
117
118        let ssml = result.unwrap();
119        assert!(ssml.contains("<emphasis level=\"strong\">"));
120    }
121}