This is a focused Android proof-of-work for a voice-first IME. The goal is not full product parity. The goal is to prove the Android wedge: system-wide voice input, on-device-first speech, direct insertion, and low-friction UX.
The project prioritizes a working demo over feature completeness, following the architecture rules strictly:
- Kotlin-first
- IME-first (not a notes app)
- On-device-first (no cloud APIs in core path)
- Clean architecture enabling future recognizer swaps
- Custom Android IME with mic-first UI
- Press-and-hold to dictate - the most natural control pattern
- On-device-first recognizer path using Android's SpeechRecognizer
- Partial transcript preview while speaking
- Two modes:
- Voice: minimal cleanup, punctuation insertion
- Message: conservative formatting with filler-word cleanup
- Direct insertion into focused text fields with InputConnection
- Clipboard fallback when direct insertion fails
- Local history with retry/copy functionality
- Mini replacement vocabulary for common voice commands
com.shehraan.voiceboard/
├── ime/
│ ├── VoiceImeService.kt # InputMethodService entry point
│ ├── ImeKeyboardView.kt # Custom keyboard UI
│ └── InputCommitter.kt # Text insertion with fallback
├── speech/
│ ├── RecognizerEngine.kt # Speech backend interface
│ ├── AndroidOnDeviceRecognizer.kt # Android on-device implementation
│ └── RecognitionState.kt # State management
├── formatting/
│ ├── FormatterPipeline.kt # Format orchestration
│ ├── VoiceModeFormatter.kt # Minimal cleanup
│ ├── MessageModeFormatter.kt # Conservative formatting
│ └── ReplacementEngine.kt # Vocabulary mapping
├── history/
│ ├── DictationHistoryEntry.kt # Data class
│ └── HistoryRepository.kt # Room persistence
├── settings/
│ ├── MainActivity.kt # Onboarding & history
│ └── SettingsViewModel.kt # Settings logic
└── util/
├── TimeMetrics.kt # Latency tracking
└── Logger.kt # Debug logging
VoiceImeService
- Owns the IME lifecycle
- Handles press-and-hold mic interaction
- Shows partial transcript previews
- Commits final text via InputCommitter
- Saves dictations to history
RecognizerEngine Interface
interface RecognizerEngine {
fun isAvailable(): Boolean
fun start(onPartial: (String) -> Unit, onFinal: (String) -> Unit, onError: (Throwable) -> Unit)
fun stop()
fun cancel()
fun destroy()
}This abstraction allows swapping Android's recognizer for WhisperCpp or Sherpa-onnx without touching IME code.
AndroidOnDeviceRecognizer
- Uses
SpeechRecognizer.createOnDeviceSpeechRecognizer() - Requires minSdk 26 (Android 8.0)
- Checks
SpeechRecognizer.isOnDeviceRecognitionAvailable()before startup - Handles partial and final results via RecognitionListener
FormatterPipeline Orchestrates formatting in two stages:
- Apply replacements ("period" -> ".", "new line" -> "\n")
- Apply mode-specific formatting (Voice vs Message)
InputCommitter Encapsulates insertion strategy:
- Try
inputConnection.commitText()(primary) - Fall back to ClipboardManager (backup)
The core value proposition is system-wide voice input. A notes app would demonstrate nothing about the critical Android integration challenges. An IME proves:
- System integration - Working with InputMethodService lifecycle
- Text insertion - Using InputConnection properly
- UI constraints - Keyboard must be compact and responsive
- Performance under pressure - Recognition UI must not lag
- Privacy - No audio leaves the device
- Latency - No network round-trips
- Reliability - Works offline, no API keys to manage
- Platform alignment - Superwhisper emphasizes on-device ML
The current implementation uses Android's on-device recognizer for speed. A production build would swap in Whisper.cpp or SherpaOnnxRecognizer behind the RecognizerEngine interface.
-
Recognition engine: Uses Android's on-device recognizer, not custom Whisper models. Enables a working demo without 500MB+ model downloads.
-
Formatting: Deterministic string manipulation, no LLM calls. Accuracy beats fancy rewriting at this stage.
-
Language support: Limited to what Android's on-device recognizer supports (typically en-US on most devices).
-
History UI: Minimal - just a list with copy/delete. No search, no sync.
-
No real-time visualizer: RMS callback exists but UI keeps it simple.
- Android Studio Giraffe or newer
- Android device or emulator running API 26+ (Android 8.0+)
- Microphone permission
# Clone or extract project
cd Voiceboard
# Open in Android Studio or build from command line
./gradlew assembleDebug
# Install to connected device
./gradlew installDebug-
Install APK on your Android device
-
Open the app - You'll see "Voiceboard" with Setup buttons
-
Grant microphone permission - Tap "Grant Microphone Permission"
-
Enable keyboard - Tap "Enable Keyboard" button (opens system settings)
- Toggle on "Voiceboard" in the list
- Confirm the security warning
-
Switch to the keyboard - Go to any app with a text field (Messages, Notes, etc.)
- Tap the text field
- Pull down the notification shade
- Tap "Select keyboard" and choose "Voiceboard"
- OR tap "Switch Keyboard" in the app
-
Dictate - Press and hold the large green mic button
- Speak clearly
- Watch the preview text appear
- Release to finalize
-
Try different modes - Tap the mode toggle (speaker icon) to switch between Voice and Message mode
-
Check history - Open the main app to see your dictation history with latency metrics
Test: Basic dictation
Input: "Hello world period"
Expected: "Hello world."
Test: Message mode formatting
Mode: Message
Input: "um I will be there at five"
Expected: "I will be there at five."
Test: Replacements
Input: "New line This is on a new line"
Expected: "\nThis is on a new line"
Test: Direct insertion
Action: Dictate into Messages app text field
Expected: Text appears directly in the field, not just copied to clipboard
- WhisperCppRecognizer: Implement the RecognizerEngine interface using whisper.cpp for better accuracy and offline first-class support
- Per-app mode activation: Remember preferred mode per input field context
- Clipboard-aware context: Detect existing text for context-aware formatting
- Model downloads: UI for downloading and managing local language models
- Language switching: Support multiple languages
- Rich history: Search, export, batch operations
- Settings screen: More granular replacement vocabulary, customization
- Pause/resume: Mid-dictation pausing
- Super Mode parity: Advanced formatting, custom vocabularies
- Sync: Optional cloud history sync
- Selected text handling: Context-aware rewrite of highlighted text
- Reliability dashboard: Error reporting, metrics
DataStore for simple key-value preferences, Room for structured history. Both are AndroidX standards, both handle threading correctly.
Tried-and-true pattern from voice assistants. Clear intent (holding = recording), natural release-to-send.
Different contexts need different cleanup. Voice mode keeps filler words for natural speech patterns. Message mode cleans up for text communication.
IME uses findViewById for code clarity. MainActivity uses ViewBinding as per standard Android practices.
- Architecture rules:
/home/sh/code/projects/superWhisper/.clinerules/architecture.md - Android docs: InputMethodService, SpeechRecognizer
- Memory bank:
memory-bank/directory for context tracking
This is a proof-of-work. For questions about the implementation approach or architecture decisions, refer to the inline comments and architecture documentation.

