tauri_plugin_tts/
models.rs

1use serde::{Deserialize, Serialize};
2
3/// Queue mode for speech requests
4#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
5#[serde(rename_all = "lowercase")]
6pub enum QueueMode {
7    /// Flush any pending speech and start speaking immediately (default)
8    #[default]
9    Flush,
10    /// Add to queue and speak after current speech finishes
11    Add,
12}
13
14/// Request to speak text
15#[derive(Debug, Deserialize, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct SpeakRequest {
18    /// The text to speak
19    pub text: String,
20    /// The language/locale code (e.g., "en-US", "pt-BR", "ja-JP")
21    #[serde(default)]
22    pub language: Option<String>,
23    /// Voice ID to use (from getVoices)
24    #[serde(default)]
25    pub voice_id: Option<String>,
26    /// Speech rate (0.25 = quarter speed, 0.5 = half speed, 1.0 = normal, 2.0 = double speed)
27    #[serde(default = "default_rate")]
28    pub rate: f32,
29    /// Pitch (0.5 = low, 1.0 = normal, 2.0 = high)
30    #[serde(default = "default_pitch")]
31    pub pitch: f32,
32    /// Volume (0.0 = silent, 1.0 = full volume)
33    #[serde(default = "default_volume")]
34    pub volume: f32,
35    /// Queue mode: "flush" (default) or "add"
36    #[serde(default)]
37    pub queue_mode: QueueMode,
38}
39
40fn default_rate() -> f32 {
41    1.0
42}
43fn default_pitch() -> f32 {
44    1.0
45}
46fn default_volume() -> f32 {
47    1.0
48}
49
50/// Response from speak command
51#[derive(Debug, Clone, Default, Deserialize, Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct SpeakResponse {
54    /// Whether speech was successfully initiated
55    pub success: bool,
56    /// Optional warning message (e.g., voice not found, using fallback)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub warning: Option<String>,
59}
60
61/// Request to stop speaking
62#[derive(Debug, Deserialize, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct StopRequest {}
65
66/// Response from stop command
67#[derive(Debug, Clone, Default, Deserialize, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct StopResponse {
70    pub success: bool,
71}
72
73/// A voice available on the system
74#[derive(Debug, Clone, Deserialize, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct Voice {
77    /// Unique identifier for the voice
78    pub id: String,
79    /// Display name of the voice
80    pub name: String,
81    /// Language code (e.g., "en-US")
82    pub language: String,
83}
84
85/// Request to get available voices
86#[derive(Debug, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase")]
88pub struct GetVoicesRequest {
89    /// Optional language filter
90    #[serde(default)]
91    pub language: Option<String>,
92}
93
94/// Response with available voices
95#[derive(Debug, Clone, Default, Deserialize, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct GetVoicesResponse {
98    pub voices: Vec<Voice>,
99}
100
101/// Request to check if TTS is currently speaking
102#[derive(Debug, Deserialize, Serialize)]
103#[serde(rename_all = "camelCase")]
104pub struct IsSpeakingRequest {}
105
106/// Response for is_speaking check
107#[derive(Debug, Clone, Default, Deserialize, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct IsSpeakingResponse {
110    pub speaking: bool,
111}
112
113/// Response for pause/resume commands
114#[derive(Debug, Clone, Default, Deserialize, Serialize)]
115#[serde(rename_all = "camelCase")]
116pub struct PauseResumeResponse {
117    pub success: bool,
118    /// Reason for failure (if success is false)
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub reason: Option<String>,
121}
122
123/// Request to preview a voice with sample text
124#[derive(Debug, Deserialize, Serialize)]
125#[serde(rename_all = "camelCase")]
126pub struct PreviewVoiceRequest {
127    /// Voice ID to preview
128    pub voice_id: String,
129    /// Optional custom sample text (uses default if not provided)
130    #[serde(default)]
131    pub text: Option<String>,
132}
133
134impl PreviewVoiceRequest {
135    /// Get the sample text, using a default if none provided
136    pub fn sample_text(&self) -> &str {
137        self.text
138            .as_deref()
139            .unwrap_or("Hello! This is a sample of how this voice sounds.")
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_speak_request_defaults() {
149        let json = r#"{"text": "Hello world"}"#;
150        let request: SpeakRequest = serde_json::from_str(json).unwrap();
151
152        assert_eq!(request.text, "Hello world");
153        assert!(request.language.is_none());
154        assert!(request.voice_id.is_none());
155        assert_eq!(request.rate, 1.0);
156        assert_eq!(request.pitch, 1.0);
157        assert_eq!(request.volume, 1.0);
158    }
159
160    #[test]
161    fn test_speak_request_full() {
162        let json = r#"{
163            "text": "Olá",
164            "language": "pt-BR",
165            "voiceId": "com.apple.voice.enhanced.pt-BR",
166            "rate": 0.8,
167            "pitch": 1.2,
168            "volume": 0.9
169        }"#;
170
171        let request: SpeakRequest = serde_json::from_str(json).unwrap();
172        assert_eq!(request.text, "Olá");
173        assert_eq!(request.language, Some("pt-BR".to_string()));
174        assert_eq!(
175            request.voice_id,
176            Some("com.apple.voice.enhanced.pt-BR".to_string())
177        );
178        assert_eq!(request.rate, 0.8);
179        assert_eq!(request.pitch, 1.2);
180        assert_eq!(request.volume, 0.9);
181    }
182
183    #[test]
184    fn test_voice_serialization() {
185        let voice = Voice {
186            id: "test-voice".to_string(),
187            name: "Test Voice".to_string(),
188            language: "en-US".to_string(),
189        };
190
191        let json = serde_json::to_string(&voice).unwrap();
192        assert!(json.contains("\"id\":\"test-voice\""));
193        assert!(json.contains("\"name\":\"Test Voice\""));
194        assert!(json.contains("\"language\":\"en-US\""));
195    }
196
197    #[test]
198    fn test_get_voices_request_optional_language() {
199        // Without language filter
200        let json1 = r#"{}"#;
201        let request1: GetVoicesRequest = serde_json::from_str(json1).unwrap();
202        assert!(request1.language.is_none());
203
204        // With language filter
205        let json2 = r#"{"language": "en"}"#;
206        let request2: GetVoicesRequest = serde_json::from_str(json2).unwrap();
207        assert_eq!(request2.language, Some("en".to_string()));
208    }
209}