Cross-platform speech recognition for Tauri 2.x. Desktop targets use whisper.cpp via whisper-rs; mobile delegates to the native OS engine (SFSpeechRecognizer on iOS, SpeechRecognizer on Android).
- One model, 99 languages — Whisper is multilingual; a single GGML model file handles English, Portuguese, Mandarin, and more
- No separate runtime to install —
whisper-rsbuilds whisper.cpp statically; there is no.so/.dylibto ship - Explicit model lifecycle — the host app controls when a model is downloaded;
start_listeningreturnsModelNotInstalledinstead of pulling hundreds of MB silently - Hardware acceleration — opt-in
metal/cuda/vulkanfeatures map to the matching whisper.cpp backend
| Platform | Engine | Model |
|---|---|---|
| iOS | SFSpeechRecognizer (Speech.framework) |
OS |
| Android | SpeechRecognizer |
OS |
| macOS | whisper.cpp via whisper-rs (Metal opt.) |
GGML |
| Windows | whisper.cpp via whisper-rs (CUDA opt.) |
GGML |
| Linux | whisper.cpp via whisper-rs (Vulkan opt.) |
GGML |
[dependencies]
tauri-plugin-stt = { version = "0.2", features = ["metal"] } # macOS
# "cuda" for NVIDIA GPU, "vulkan" for cross-vendor GPU, omit for CPUnpm install tauri-plugin-stt-apiRegister the plugin:
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_stt::init())
.run(tauri::generate_context!())
.unwrap();
}{ "permissions": ["stt:default"] }| id | Size | Notes |
|---|---|---|
tiny |
75 MB | fastest |
base |
142 MB | balanced ⭐ |
small |
466 MB | accurate |
medium |
1.5 GB | very accurate |
large-v3-turbo |
1.6 GB | fast & accurate (advanced) |
large-v3 |
3.0 GB | most accurate |
Files are fetched from HuggingFace (ggerganov/whisper.cpp) and stored under <app_data_dir>/whisper-models/. The active model is persisted to whisper-models/active.txt.
list_models()→{ models, active, total_disk_bytes }install_model(id)— downloads and emitsstt://download-progresseventsremove_model(id)— deletes file; clears active marker if neededset_active_model(id)— sets which installed modelstart_listeningloadsunload_model()— drops the loaded Whisper context from memory; fails while listening or transcribingstart_listening({ language?, max_duration? })— begins a push-to-talk sessionstop_listening()— runs Whisper over captured audio and emits a final resultis_available()—trueonly when a model is installed and readyget_supported_languages()— curated list of UI-facing localescheck_permission()/request_permission()— microphone permission helpers
stt://download-progress—{ status, modelId, model, progress, downloaded?, total? }stt://result—{ transcript, isFinal, confidence }stt://error/plugin:stt:error—{ code, message, details? }(codes follow theSttErrorCodeunion, e.g.NO_SPEECH,AUDIO_ERROR)plugin:stt:stateChange—{ state, isAvailable, language }(idleis emitted only after transcription finishes)
- Whisper is not a streaming recogniser. The plugin buffers audio during recording and runs a single inference pass on
stop_listening. The UX is push-to-talk, not live transcription. - Audio is captured at the device default rate, downmixed to mono, then decimated to 16 kHz with nearest-neighbour. Whisper is robust enough that a higher-quality resampler makes no measurable difference.
- Inference uses
min(available_parallelism(), 4)threads — beyond that whisper.cpp shows diminishing returns, and we want headroom for the UI.
The mobile bridges expose the same JS API surface, but list_models returns an empty list and install_model / remove_model / set_active_model / unload_model are no-ops: the OS engine has no downloadable model concept. Use is_available to gate UI — on iOS/Android it reflects actual recogniser availability.
MIT