tauri-plugin-tts-api
v0.2.0
Published
Native text-to-speech API for Tauri with multi-language and voice selection
Maintainers
Readme
Tauri Plugin TTS (Text-to-Speech)
Cross-platform native Text-to-Speech (TTS) plugin for Tauri 2.x using OS synthesizers: WinRT (Windows), AVSpeechSynthesizer (macOS/iOS), speech-dispatcher (Linux), and TextToSpeech (Android).
📑 Table of Contents
✨ Features
- 🔊 Native Voice Synthesis: 100% offline, zero-latency speech synthesis powered by OS engines.
- 🎛️ Normalized Controls: Unified rate, pitch, and volume scales across all platforms.
- 🗣️ Voice Enumeration: Discover installed voices, filter by locale, and preview samples.
- 📋 Utterance Queuing: Choose between
flush(interrupt) andadd(queue sequentially). - 📡 Lifecycle Events: Track
speech:start,speech:finish,speech:cancel, andspeech:error. - 📱 Mobile Background Audio: Continue speaking when the screen locks or app is minimized.
📊 Platform Matrix
| Platform | Native Engine | Pause/Resume | Background Audio |
| :--- | :--- | :---: | :---: |
| Windows | WinRT (SpeechSynthesis) | — | — |
| macOS | AVSpeechSynthesizer | — | — |
| Linux | speech-dispatcher | — | — |
| iOS | AVSpeechSynthesizer | ✅ | ✅ |
| Android | android.speech.tts.TextToSpeech | — | ✅ |
📦 Installation
# Rust
cargo add tauri-plugin-tts
# JavaScript / TypeScript
npm install tauri-plugin-tts-api
# or: pnpm add / yarn add / bun add tauri-plugin-tts-api⚙️ Setup
1. Register Plugin in Rust
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_tts::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}2. Configure Capabilities
Add the plugin permission to src-tauri/capabilities/default.json:
{
"permissions": [
"core:default",
"tts:default"
]
}[!NOTE] Android
<queries>declarations for TTS services are merged automatically by the plugin during build.
💻 Usage
import {
speak,
stop,
getVoices,
previewVoice,
onSpeechEvent,
} from "tauri-plugin-tts-api";
// 1. Basic speech
await speak({ text: "Hello, world!" });
// 2. Custom rate, pitch, language, and queue mode
await speak({
text: "Olá, mundo!",
language: "pt-BR",
rate: 1.0, // 0.1 to 4.0 (1.0 = normal)
pitch: 1.1, // 0.5 to 2.0 (1.0 = normal)
volume: 1.0, // 0.0 to 1.0
queueMode: "flush", // 'flush' (interrupt) | 'add' (queue)
});
// 3. Discover and preview voices
const voices = await getVoices("en");
if (voices.length > 0) {
const preview = await previewVoice({ voiceId: voices[0].id });
// success is false when the voice is not installed - the preview is silent otherwise
if (!preview.success) console.warn(preview.warning);
}
// 4. Listen for completion event
const unlisten = await onSpeechEvent("speech:finish", (e) => {
console.log("Finished utterance:", e.id);
});📚 API Reference
| Function | Parameters | Return Type | Description |
| :--- | :--- | :--- | :--- |
| speak(options) | SpeakOptions | Promise<SpeakResponse> | Synthesizes text. Resolves once the utterance is accepted by the engine. |
| stop() | none | Promise<void> | Halts active speech synthesis. |
| getVoices(language?) | string? | Promise<Voice[]> | Returns installed voices, filtered by locale prefix. |
| previewVoice(options) | PreviewVoiceOptions | Promise<SpeakResponse> | Speaks a short sample using voice ID. |
| isSpeaking() | none | Promise<boolean> | Returns true if synthesizer is speaking. |
| isInitialized() | none | Promise<{ initialized, voiceCount }> | Checks if mobile synthesizer is initialized. |
| pauseSpeaking() / resumeSpeaking() | none | Promise<PauseResumeResponse> | Pauses / resumes speech (iOS only). |
| setBackgroundBehavior(opts) | { continueInBackground } | Promise<void> | Controls lock-screen behavior (Mobile). |
| onSpeechEvent(type, handler) | SpeechEventType, Function | Promise<UnlistenFn> | Subscribes to speech events. |
SpeakResponse
speak() and previewVoice() both resolve with:
| Field | Type | Description |
| :--- | :--- | :--- |
| success | boolean | false when nothing will be spoken — currently only previewVoice() with a voice that is not installed. |
| warning | string? | Set when the requested voiceId or language was unavailable and the system default was used instead. Speech still happens, so check this if the user picked a specific voice. |
| utteranceId | string? | Matches the id on this utterance's lifecycle events. Read it to correlate a call with its events instead of racing speech:start. |
const { warning, utteranceId } = await speak({ text: "Hi", voiceId: savedId });
if (warning) console.warn(warning); // e.g. voice uninstalled since it was savedSpeech Events
Subscribe with onSpeechEvent(type, handler). Every payload carries eventType, and
id whenever the platform can attribute the event to a specific utterance.
| Event | When | Platforms |
| :--- | :--- | :---: |
| speech:start | The engine began speaking this utterance. | All |
| speech:finish | The utterance completed on its own. | All |
| speech:cancel | The utterance was stopped — by stop(), or by a flush that replaced it. Carries interrupted on Android. | All |
| speech:error | Synthesis failed. Carries error. | All |
| speech:pause | Speech paused. Carries reason for automatic pauses (route_change, interruption_ended). | iOS |
| speech:resume | Speech resumed after a pause. | iOS |
| speech:interrupted | A phone call or another app took the audio session. Carries reason on Android. | iOS, Android |
| speech:backgroundPause | The app backgrounded while continueInBackground was false. | iOS, Android |
[!NOTE]
speech:cancelandspeech:finishare mutually exclusive for a givenid, so a state machine can treat either one as terminal.
Rate, Pitch and Volume
1.0 always means "this platform's normal". The plugin maps that onto each backend's own
scale, which differ wildly (AVFoundation rate is 0.0–1.0 with 0.5 normal, WinRT is
0.5–6.0 with 1.0 normal, speech-dispatcher is -100–100 with 0 normal), so the
same value sounds equivalent everywhere.
| Parameter | Range | Normal |
| :--- | :--- | :--- |
| rate | 0.1 – 4.0 | 1.0 |
| pitch | 0.5 – 2.0 | 1.0 |
| volume | 0.0 – 1.0 | 1.0 |
Out-of-range values are clamped rather than rejected. text is limited to
MAX_TEXT_LENGTH (10,000 UTF-8 bytes), exported from the JavaScript API.
🛠️ Troubleshooting
- Linux "Speech Dispatcher not available": Install the daemon via
sudo apt install speech-dispatcherorsudo dnf install speech-dispatcher. - Android Missing Voices: Download language packs in Settings → Accessibility → Text-to-Speech Output → Google TTS.
- iOS Background Mode: To continue speaking with the screen locked, add
UIBackgroundModeswithaudioinInfo.plist. - A saved
voiceIdspeaks in the wrong voice: the voice was uninstalled.speak()falls back to the system default and reports it inwarning— re-rungetVoices()and let the user pick again. - Android "TTS engine failed to initialize": no text-to-speech engine is installed. Install one from Settings → Accessibility → Text-to-Speech Output;
isInitialized()reportsfalseuntil then.
🌍 Used By
See USED_BY.md for projects and organizations using this plugin in production.
📄 License
MIT © Breno Gonzaga
