Skip to main content

tauri_plugin_stt/
models.rs

1use serde::{Deserialize, Serialize};
2
3/// Language code for speech recognition (e.g., "en-US", "pt-BR", "ja-JP")
4pub type LanguageCode = String;
5
6/// Configuration for starting speech recognition
7#[derive(Debug, Clone, Default, Deserialize, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct ListenConfig {
10    /// Language code for recognition (e.g., "en-US", "pt-BR")
11    /// If not specified, uses device default language
12    #[serde(default)]
13    pub language: Option<LanguageCode>,
14
15    /// Whether to return interim (partial) results
16    #[serde(default, rename = "interimResults")]
17    pub interim_results: bool,
18
19    /// Whether to continue listening after getting a result
20    /// If false, stops after first final result
21    #[serde(default)]
22    pub continuous: bool,
23
24    /// Maximum duration to listen in milliseconds (0 = no limit)
25    #[serde(default, rename = "maxDuration")]
26    pub max_duration: u32,
27
28    /// Maximum number of alternative transcriptions
29    #[serde(default, rename = "maxAlternatives")]
30    pub max_alternatives: Option<u32>,
31
32    /// Use on-device recognition only (iOS 13+, no network required)
33    /// When true, recognition works offline but may be less accurate
34    #[serde(default, rename = "onDevice")]
35    pub on_device: bool,
36}
37
38/// Recognition state
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "lowercase")]
41#[derive(Default)]
42pub enum RecognitionState {
43    /// Not currently listening
44    #[default]
45    Idle,
46    /// Actively listening for speech
47    Listening,
48    /// Processing audio (may briefly occur between utterances)
49    Processing,
50}
51
52/// A speech recognition result
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct RecognitionResult {
56    /// The recognized text
57    pub transcript: String,
58
59    /// Whether this is a final result (vs interim/partial)
60    pub is_final: bool,
61
62    /// Confidence score (0.0 to 1.0), if available
63    #[serde(default)]
64    pub confidence: Option<f32>,
65
66    /// Base64-encoded WAV audio of the utterance that produced this
67    /// result (desktop only). `None` on mobile / when unavailable.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub audio_data: Option<String>,
70}
71
72/// Current status of speech recognition
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct RecognitionStatus {
76    /// Current state
77    pub state: RecognitionState,
78
79    /// Whether STT is available on this device
80    pub is_available: bool,
81
82    /// Current language being used
83    #[serde(default)]
84    pub language: Option<LanguageCode>,
85}
86
87/// Supported language information
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct SupportedLanguage {
91    /// Language code (e.g., "en-US")
92    pub code: LanguageCode,
93
94    /// Human-readable name (e.g., "English (United States)")
95    pub name: String,
96
97    /// Whether the model for this language is installed locally (desktop only)
98    #[serde(default)]
99    pub installed: Option<bool>,
100}
101
102/// Permission status
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum PermissionStatus {
106    /// Permission has been granted
107    Granted,
108    /// Permission has been denied
109    Denied,
110    /// Permission hasn't been requested yet
111    Unknown,
112}
113
114/// Response for permission check
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct PermissionResponse {
118    /// Microphone permission status
119    pub microphone: PermissionStatus,
120
121    /// Speech recognition permission status (iOS/macOS specific)
122    pub speech_recognition: PermissionStatus,
123}
124
125/// Response for availability check
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct AvailabilityResponse {
129    /// Whether STT is available
130    pub available: bool,
131
132    /// Reason if not available
133    #[serde(default)]
134    pub reason: Option<String>,
135}
136
137/// Response for supported languages
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct SupportedLanguagesResponse {
141    /// List of supported languages
142    pub languages: Vec<SupportedLanguage>,
143}
144
145/// Unified error codes for cross-platform consistency
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
148#[derive(Default)]
149pub enum SttErrorCode {
150    /// No error
151    #[default]
152    None,
153    /// Speech recognition service not available
154    NotAvailable,
155    /// Microphone permission denied
156    PermissionDenied,
157    /// Speech recognition permission denied (iOS)
158    SpeechPermissionDenied,
159    /// Network error (server-based recognition)
160    NetworkError,
161    /// Audio recording error
162    AudioError,
163    /// Recognition timed out (maxDuration reached)
164    Timeout,
165    /// No speech detected
166    NoSpeech,
167    /// Language not supported
168    LanguageNotSupported,
169    /// Recognition was cancelled
170    Cancelled,
171    /// Already listening
172    AlreadyListening,
173    /// Not currently listening
174    NotListening,
175    /// Service busy
176    Busy,
177    /// No Whisper model has been downloaded yet
178    ModelNotInstalled,
179    /// Unknown error
180    Unknown,
181}
182
183impl SttErrorCode {
184    /// Get a human-readable description of the error
185    pub fn description(&self) -> &'static str {
186        match self {
187            Self::None => "No error",
188            Self::NotAvailable => "Speech recognition is not available on this device",
189            Self::PermissionDenied => "Microphone permission was denied",
190            Self::SpeechPermissionDenied => "Speech recognition permission was denied",
191            Self::NetworkError => "Network error during recognition",
192            Self::AudioError => "Error accessing audio input",
193            Self::Timeout => "Recognition timed out",
194            Self::NoSpeech => "No speech was detected",
195            Self::LanguageNotSupported => "The requested language is not supported",
196            Self::Cancelled => "Recognition was cancelled",
197            Self::AlreadyListening => "Already listening for speech",
198            Self::NotListening => "Not currently listening",
199            Self::Busy => "Speech recognition service is busy",
200            Self::ModelNotInstalled => "No speech recognition model has been downloaded",
201            Self::Unknown => "An unknown error occurred",
202        }
203    }
204
205    /// Get the numeric code for this error
206    pub fn code(&self) -> i32 {
207        match self {
208            Self::None => 0,
209            Self::NotAvailable => -1,
210            Self::PermissionDenied => -2,
211            Self::SpeechPermissionDenied => -3,
212            Self::NetworkError => -4,
213            Self::AudioError => -5,
214            Self::Timeout => -6,
215            Self::NoSpeech => -7,
216            Self::LanguageNotSupported => -8,
217            Self::Cancelled => -9,
218            Self::AlreadyListening => -10,
219            Self::NotListening => -11,
220            Self::Busy => -12,
221            Self::ModelNotInstalled => -13,
222            Self::Unknown => -99,
223        }
224    }
225}
226
227/// Structured error event for frontend consumption
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct SttError {
231    /// Error code for programmatic handling
232    pub code: SttErrorCode,
233    /// Human-readable error message
234    pub message: String,
235    /// Platform-specific error details (optional)
236    #[serde(default)]
237    pub details: Option<String>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct WhisperModelInfo {
243    /// Stable identifier (`tiny`, `tiny.en`, `base`, `base.en`,
244    /// `small`, `small.en`, `medium`, `medium.en`, `large-v3-turbo`,
245    /// `large-v3`).
246    pub id: String,
247    /// Human-readable name shown in the model manager.
248    pub display_name: String,
249    /// Approximate on-disk size in megabytes — used for confirmation
250    /// dialogs ("Download 142 MB?") and for the disk-usage summary.
251    pub size_mb: u32,
252    /// Approximate working-set memory in megabytes (whisper.cpp's
253    /// published "required memory" — covers RAM on CPU, VRAM on GPU).
254    /// Drives the "your device has only X MB" gate so we never let
255    /// a user download a model their machine can't actually run.
256    pub required_memory_mb: u32,
257    /// Whether the binary is currently present in `app_data_dir`.
258    pub installed: bool,
259    /// Whether this model is the one `start_listening` will load.
260    pub active: bool,
261    /// Marks the suggested default for first-time users. Exactly one
262    /// model in the catalogue carries this flag.
263    pub recommended: bool,
264    /// Short qualitative label for the speed ↔ accuracy trade-off,
265    /// e.g. `"fastest"`, `"balanced"`, `"most accurate"`. Lets the UI
266    /// stay in sync with the catalogue without owning copy.
267    pub tier: String,
268    /// `Some("en")` for English-optimised variants (`*.en`), `None`
269    /// for the multilingual default models. The frontend prefers an
270    /// `.en` variant when the course's declared language is English.
271    #[serde(default)]
272    pub language: Option<String>,
273    /// `false` when the local machine doesn't have enough RAM/VRAM to
274    /// load this model. Drives the install button's disabled state and
275    /// the "Not enough memory" hint in the UI.
276    pub fits_in_memory: bool,
277    /// Power-user model (currently the `large` family). Hidden from
278    /// the default catalogue listing — surfaces only when the caller
279    /// explicitly asks for advanced models.
280    pub advanced: bool,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct WhisperModelsResponse {
286    /// Catalogue ordered from smallest to largest.
287    pub models: Vec<WhisperModelInfo>,
288    /// Currently active model id (`None` if none installed yet).
289    #[serde(default)]
290    pub active: Option<String>,
291    /// Total bytes occupied by every installed model. Drives the
292    /// "Disk usage" line in the settings page.
293    pub total_disk_bytes: u64,
294    /// Total physical RAM (in MB) the host machine reports. The UI
295    /// shows this next to each model's `requiredMemoryMb` so the user
296    /// understands *why* a model is greyed out.
297    pub system_memory_mb: u32,
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_listen_config_defaults() {
306        let config: ListenConfig = serde_json::from_str("{}").unwrap();
307        assert!(config.language.is_none());
308        assert!(!config.interim_results);
309        assert!(!config.continuous);
310        assert_eq!(config.max_duration, 0);
311    }
312
313    #[test]
314    fn test_listen_config_full() {
315        let json = r#"{
316            "language": "pt-BR",
317            "interimResults": true,
318            "continuous": true,
319            "maxDuration": 30
320        }"#;
321        let config: ListenConfig = serde_json::from_str(json).unwrap();
322        assert_eq!(config.language, Some("pt-BR".to_string()));
323        assert!(config.interim_results);
324        assert!(config.continuous);
325        assert_eq!(config.max_duration, 30);
326    }
327
328    #[test]
329    fn test_recognition_state_serialization() {
330        assert_eq!(
331            serde_json::to_string(&RecognitionState::Idle).unwrap(),
332            "\"idle\""
333        );
334        assert_eq!(
335            serde_json::to_string(&RecognitionState::Listening).unwrap(),
336            "\"listening\""
337        );
338        assert_eq!(
339            serde_json::to_string(&RecognitionState::Processing).unwrap(),
340            "\"processing\""
341        );
342    }
343
344    #[test]
345    fn test_recognition_result() {
346        let result = RecognitionResult {
347            transcript: "Hello world".to_string(),
348            is_final: true,
349            confidence: Some(0.95),
350            audio_data: None,
351        };
352        let json = serde_json::to_string(&result).unwrap();
353        assert!(json.contains("\"transcript\":\"Hello world\""));
354        assert!(json.contains("\"isFinal\":true"));
355        assert!(json.contains("\"confidence\":0.95"));
356    }
357
358    #[test]
359    fn test_permission_status_serialization() {
360        assert_eq!(
361            serde_json::to_string(&PermissionStatus::Granted).unwrap(),
362            "\"granted\""
363        );
364        assert_eq!(
365            serde_json::to_string(&PermissionStatus::Denied).unwrap(),
366            "\"denied\""
367        );
368        assert_eq!(
369            serde_json::to_string(&PermissionStatus::Unknown).unwrap(),
370            "\"unknown\""
371        );
372    }
373}