Local speech-to-text keyboard for iOS.
Your voice is processed entirely on your iPhone – nothing leaves your device.
Version 1.0.0 · iOS 17.0+ · Apple Silicon
Also available for macOS
Wave is a custom iOS keyboard with a dictation button. Tap the mic, speak, and your words appear as text in any app – Messages, Notes, Mail, or anything with a text field.
Unlike cloud-based dictation, Wave runs WhisperKit (OpenAI's Whisper model) entirely on-device. No internet required, no data sent anywhere.
- Switch to the Wave keyboard (globe icon)
- Tap the mic button
- Wave app opens and starts recording
- Speak, then tap Done
- Switch back to your app – text is inserted automatically
Apple blocks microphone access in all keyboard extensions (privacy policy, not a bug). So Wave uses the same pattern as Wispr Flow and other dictation keyboards: the keyboard launches the main app for recording, then passes the result back through shared storage.
┌─────────────────────┐ wave://dictate ┌─────────────────────┐
│ Keyboard Extension │ ────────────────────▶ │ Wave App │
│ │ │ │
│ • Mic button │ App Group │ • AVAudioEngine │
│ • Status display │ ◀──────────────────── │ • WhisperKit │
│ • Polls for results │ (transcription) │ • Filler filter │
│ │ │ • History │
│ │ │ • Settings │
└─────────────────────┘ └─────────────────────┘
Shared via App Group (group.com.santiagoalonso.wave):
- Transcription results
- Dictation state (idle → recording → transcribing → done)
- Settings (model, language, filler toggle)
- History
WaveKeyboard/
├── Shared/ # Code shared between both targets
│ ├── AudioRecorder.swift # AVAudioEngine recording (main app only)
│ ├── DictationState.swift # State enum for the pipeline
│ ├── FillerFilter.swift # Removes um, uh, you know, etc.
│ ├── SharedStorage.swift # App Group read/write bridge
│ ├── Transcriber.swift # WhisperKit wrapper (main app only)
│ └── TranscriptionHistory.swift # JSON history in shared container
│
├── WaveApp/ # Main app target
│ ├── WaveApp.swift # @main, deep link handler
│ ├── ContentView.swift # Home screen + setup instructions
│ ├── DictationView.swift # Recording + transcription UI
│ ├── OnboardingView.swift # First-run setup wizard
│ ├── SettingsView.swift # Model, language, filler, history
│ └── HistoryView.swift # Past transcriptions
│
├── WaveKeyboardExtension/ # Keyboard extension target
│ ├── KeyboardViewController.swift # UIInputViewController + polling
│ └── KeyboardView.swift # SwiftUI keyboard UI
│
└── project.yml # XcodeGen spec
- Xcode 16+
- iOS 17.0+ device (Simulator works for UI, but keyboard extensions need a real device)
- XcodeGen (
brew install xcodegen)
cd WaveKeyboard
xcodegen generate
open WaveKeyboard.xcodeprojSet your development team in Signing & Capabilities for both targets, then build and run on your iPhone.
- Settings → General → Keyboard → Keyboards → Add New Keyboard → Wave
- Tap Wave in the list → enable Allow Full Access
- In any text field, long-press the globe/emoji button → select Wave
| Setting | Default | Options |
|---|---|---|
| Speech model | base (~140 MB) | tiny, base, small |
| Detect language | Auto-detect | auto + 9 languages |
| Output language | Original | Original, English (translate) |
| Filler removal | On | On/Off |
| Copy to clipboard | On | On/Off |
| History | On | On/Off |
This project reuses core logic from the macOS Wave menu bar app:
| File | Reuse | Changes |
|---|---|---|
| FillerFilter.swift | ~100% | Reads settings from SharedStorage instead of UserDefaults |
| Transcriber.swift | ~90% | os.Logger, parameterized model/language, @Published state |
| AudioRecorder.swift | ~85% | Added AVAudioSession setup, audio level publishing |
| TranscriptionHistory.swift | Logic | Rewritten for App Group container |
Found a bug or have a feature request? Open an issue.
Made by santiagoalonso.com