An on-device Android speech-to-text provider backed by
Moonshine. It implements android.speech.RecognitionService, so any
app can use it through the standard SpeechRecognizer API, and it appears in
system voice-input pickers. All recognition happens locally; nothing leaves
the device.
scripts/fetch-models.sh # ~51 MB into app/src/main/assets (once)
./gradlew :app:assembleDebug # or :app:assembleRelease (R8-minified)Release builds are signed with the release keystore when the
EARSHINE_KEYSTORE_FILE / EARSHINE_KEYSTORE_PASSWORD / EARSHINE_KEY_ALIAS
/ EARSHINE_KEY_PASSWORD environment variables are set, and fall back to the
debug key otherwise so a local adb install just works. Pushing a v* tag
runs .github/workflows/release.yml, which builds a signed APK from repo
secrets and attaches it to a GitHub release.
Requirements: JDK 17โ21 for Gradle (export JAVA_HOME=...), Android SDK
(platform 35). If you skip fetch-models.sh, the APK still works โ it
downloads the same files from download.moonshine.ai on first launch
instead of unpacking them from assets.
Constraints inherited from the upstream AAR: arm64-v8a only and minSdk 35 (Android 15). Use a physical device or an arm64 emulator image.
From another app:
val recognizer = SpeechRecognizer.createSpeechRecognizer(
context,
ComponentName("dev.earshine", "dev.earshine.MoonshineRecognitionService"),
)
recognizer.setRecognitionListener(listener)
recognizer.startListening(Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
})- Default sessions are single-utterance: Moonshine's VAD endpoints the
speech,
onResultsfires, done. - Pass
RecognizerIntent.EXTRA_SEGMENTED_SESSIONfor continuous dictation: each completed line arrives viaonSegmentResultsuntil you callstopListening(). - English only (
en*); otherEXTRA_LANGUAGEvalues getERROR_LANGUAGE_NOT_SUPPORTED.
The bundled demo activity does exactly this against the service and also
serves as the first-run screen that provisions the model and requests the
microphone permission (the audio is captured in this app's process, so
Earshine itself must hold RECORD_AUDIO).
- Voice IME (
VoiceInputMethodService): a voice input method that keyboards with a mic key (HeliBoard, AnySoftKeyboard, โฆ) can switch to. Enable it under Settings โ System โ Keyboard โ On-screen keyboards. It runs a continuous segmented session, shows the in-progress line as composing text, and commits each completed line. RECOGNIZE_SPEECHdialog (RecognizeSpeechActivity): handles the classicRecognizerIntent.ACTION_RECOGNIZE_SPEECHstartActivityForResult contract, returning the transcript inRecognizerIntent.EXTRA_RESULTS.
tiny-streaming-en is the default. For higher accuracy at ~2ร the size and
compute, base-en (non-streaming) or base-streaming-en also work:
run scripts/fetch-models.sh base-en and update Models.DEFAULT in
app/src/main/java/dev/earshine/ModelStore.kt (name, arch constant, file
list โ see the script header). Non-English base models exist too, but the
service currently advertises English only.