feat(speech-recognition): native streaming speech-to-text (KMP) - #724
Draft
kdroidFilter wants to merge 1 commit into
Draft
kdroidFilter wants to merge 1 commit into
kdroidFilter wants to merge 1 commit into
Conversation
New Kotlin Multiplatform module (jvm, android, iosArm64, iosSimulatorArm64) inspired by robius-speech: - Desktop: Rust JNI bridge (nucleus_speech) over robius-speech, pinned by commit — SAPI dictation on Windows, SFSpeechRecognizer on macOS. Linux ships no library and reports itself unsupported. - Android: Kotlin port of robius' SpeechRecognizer backend, with an Activity tracker provider and a headless permission fragment. - iOS: Kotlin/Native port of robius' SFSpeechRecognizer + AVAudioEngine bridge. - Common: session rules (one live session, one terminal event, late callbacks dropped), Dictation (text field edits, UTF-16 offsets) and segment tracking. Build: nucleus.native-module now supports KMP modules (nucleusNative.jvmResources), and check runs the per-source-set detekt tasks on KMP. CI builds and verifies the Windows and macOS libraries. robius-speech notice added (THIRD_PARTY_NOTICES §5).
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New Kotlin Multiplatform module
speech-recognition(dev.nucleusframework:nucleus.speech-recognition), inspired byrobius-speech. Rust is used for desktop only.robius-speechrobius-speech(its Swift bridge)isSupported == falseSpeechRecognizerSpeechRecognition.start(options) { event -> }returns aSpeechSession(stop()/cancel(),AutoCloseable). Events:Started,Transcript(text, isFinal),AudioLevel, then exactly one terminal event,StoppedorError(kind, message). Only one session runs at a time.Dictation: port ofdictation.rsthat turns transcripts into text-field edits, using UTF-16 offsets (ComposeTextRange).robius-speech: repository dependency pinned byrev, since it is not published on crates.io.Build / CI
nucleus.native-modulenow supports KMP modules throughnucleusNative.jvmResources. Natives stay insrc/main/resources/nucleus/native, where CI already puts and verifies them.detekttask isNO-SOURCE, sochecknow runs the per-source-set detekt tasks.build-natives.yaml,pre-merge.yamlandpublish-maven.yamlnow build and verify the Windows (x64/ARM64) and macOS (arm64/x64) libraries.robius-speechMIT notice is in THIRD_PARTY_NOTICES §5 and ships in the JVM JAR and the AAR.Documentation
Installation
implementation("dev.nucleusframework:nucleus.speech-recognition:<version>")Same artifact for every target (Gradle metadata picks the JVM, Android or iOS variant).
Usage
start()returns at once and asks for the permissions itself;Startedmeans the microphone isrecording, a refusal arrives as
Error(PermissionDenied).StoppedorError— unless it is cancelled,after which nothing is delivered.
SpeechException(Busy)otherwise).SpeechRecognition.cancelAll()is for suspending or quitting.
Windows): hop to the UI thread yourself.
on across utterances until stopped. Speech stays plain text: saying "enter" types "enter".
Putting the words into a text field
Dictationturns transcripts into edits, without knowing any UI toolkit. Offsets are UTF-16indices, like
Stringand Compose'sTextRange:When the user is about to edit the field (a keystroke, a click moving the caret), call
interrupt()first andsettle(text, selectionStart, selectionEnd)once the edit has landed:dictation resumes at the caret without losing or repeating a word.
Replacement.continuestells arevision of the previous edit apart, for grouping undo.
Platform setup
macOS. Recognition needs an app bundle whose Info.plist declares both usage descriptions — a
bare
./gradlew runreportsPermissionDenied("Launch the application from its .app bundle").A hardened-runtime (notarized) app also needs the
com.apple.security.device.audio-inputentitlement, which Nucleus' default entitlements do not grant:
nucleus.application { nativeDistributions { macOS { infoPlist { extraKeysRawXml = """ <key>NSMicrophoneUsageDescription</key> <string>Dictation uses the microphone.</string> <key>NSSpeechRecognitionUsageDescription</key> <string>Dictation transcribes your speech.</string> """.trimIndent() } entitlementsFile.set(project.file("entitlements.plist")) // + device.audio-input } } }Events are delivered on the main dispatch queue, which the Tao event loop drains; a headless app
without a Cocoa run loop on the main thread receives none.
Windows. Needs an installed Windows speech recognition language and microphone access
(Settings › Privacy › Microphone, "desktop apps"). No package identity is required.
Android. The library manifest already declares
RECORD_AUDIOand theRecognitionServicequery, and a content provider tracks the foreground Activity. The runtime permission prompt is
handled for you (headless fragment); a session ends when its Activity pauses.
iOS. Declare
NSMicrophoneUsageDescriptionandNSSpeechRecognitionUsageDescription; bothpermissions are requested for you. Recognition prefers on-device models and restarts its task
before Apple's one-minute limit.
Development
src/main/native(Rust crate;windows/build.bat,macos/build.sh), built by./gradlew :speech-recognition:buildNativeWindows/buildNativeMacOs.robius-speechis a gitdependency pinned by commit; bump
revinCargo.tomlandcargo update -p robius-speech../gradlew :speech-recognition:jvmTest -Dnucleus.speech.live=trueopens the real microphone fora few seconds (Windows/macOS).
Verified
:speech-recognition:checkpasses: 24 JVM tests, ktlint, detekt per source set andapiCheck.:fs-watcher:checkalso passes after the plugin change.Error(Audio, 0x8004503A).Not verified yet
./gradlew :speech-recognition:jvmTest -Dnucleus.speech.live=true.appbundle. The app needsNSMicrophoneUsageDescription,NSSpeechRecognitionUsageDescriptionand thecom.apple.security.device.audio-inputentitlement, which the default entitlements lack (documented in the module README).