From e13b0618db7e6a01ce9de6667b3711a55ba0a8ec Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Tue, 13 Oct 2026 15:15:41 -0700 Subject: [PATCH] voice: recommend Canary for the final words, at a third of Parakeet large's size --- .../app/seb3thehacker/gearslip/speech/SpeechEngine.kt | 4 ++-- .../app/seb3thehacker/gearslip/speech/SpeechModels.kt | 10 ++++++---- .../java/app/seb3thehacker/gearslip/ui/IntroScreen.kt | 2 +- .../app/seb3thehacker/gearslip/ui/VoiceModelViews.kt | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechEngine.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechEngine.kt index 525c55c..8684259 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechEngine.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechEngine.kt @@ -67,7 +67,7 @@ object SpeechEngine { private val worker = Executors.newSingleThreadExecutor { Thread(it, "gearslip-speech") } /** - * Parakeet large takes seconds to load. On [worker], the first press after connecting + * The final model takes seconds to load. On [worker], the first press after connecting * waited behind it before listening at all; here it loads while the driver talks. */ private val finalWorker = Executors.newSingleThreadExecutor { Thread(it, "gearslip-speech-final") } @@ -80,7 +80,7 @@ object SpeechEngine { @Volatile private var loadedFinal: SpeechModels.Model? = null /** - * Parakeet large holds most of the ~900 MB the two models take, so it goes after a few + * The final model holds most of the memory the two models take, so it goes after a few * minutes without a command. Kroko stays: it's small, and keeps listening instant. */ private val idleUnload = Runnable { unloadFinal("after ${IDLE_UNLOAD_MS / 60_000} minutes without use") } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechModels.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechModels.kt index 11a2957..3546aed 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechModels.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/speech/SpeechModels.kt @@ -51,12 +51,12 @@ object SpeechModels { plainFiles = false, ), CANARY( - Role.FINAL, "canary-180m-en", "Canary", "In between: better than Parakeet small, quicker than large. NVIDIA NeMo, CC BY 4.0", 198, + Role.FINAL, "canary-180m-en", "Canary", "Recommended. Quick, and hears names and places well. NVIDIA NeMo, CC BY 4.0", 198, "sherpa-onnx-nemo-canary-180m-flash-en-es-de-fr-int8", "encoder.int8.onnx", "decoder.int8.onnx", "", modelType = "canary", ), PARAKEET_LARGE( - Role.FINAL, "parakeet-0.6b-en", "Parakeet large", "Recommended. Hears names and places best. NVIDIA NeMo, CC BY 4.0", 631, + Role.FINAL, "parakeet-0.6b-en", "Parakeet large", "Hears names best, but slower and three times the size. NVIDIA NeMo, CC BY 4.0", 631, "sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "encoder.int8.onnx", "decoder.int8.onnx", "joiner.int8.onnx", modelType = "nemo_transducer", ), @@ -82,7 +82,7 @@ object SpeechModels { private val states = Model.entries.associateWith { MutableStateFlow(State.Missing) } private val _selected = MutableStateFlow(Model.KROKO) val selected: StateFlow = _selected.asStateFlow() - private val _final = MutableStateFlow(Model.PARAKEET_LARGE) + private val _final = MutableStateFlow(Model.CANARY) /** The model that rereads the whole phrase at the end; null to keep the live model's words. */ val final: StateFlow = _final.asStateFlow() @@ -91,7 +91,9 @@ object SpeechModels { fun init(context: Context) { val id = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_MODEL, null) _selected.value = Model.entries.firstOrNull { it.id == id && it.role == Role.LIVE } ?: Model.KROKO - val finalId = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_FINAL, Model.PARAKEET_LARGE.id) + // Canary is the default. A driver who never chose but already has Parakeet large keeps it. + val finalId = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_FINAL, null) + ?: if (isReady(context, Model.PARAKEET_LARGE) && !isReady(context, Model.CANARY)) Model.PARAKEET_LARGE.id else Model.CANARY.id _final.value = Model.entries.firstOrNull { it.id == finalId && it.role == Role.FINAL } Model.entries.forEach { m -> if (isReady(context, m)) states.getValue(m).value = State.Ready } // Models an older build offered and this one doesn't: hundreds of MB nobody can use. diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt index a29d94b..a7fe37b 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt @@ -402,7 +402,7 @@ private fun BadgeStep( private fun VoiceStep(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> TextUnit, key: IntroStep) { val context = LocalContext.current remember { SpeechModels.init(context) } - val needed = listOf(SpeechModels.Model.KROKO, SpeechModels.Model.PARAKEET_LARGE) + val needed = listOf(SpeechModels.Model.KROKO, SpeechModels.Model.CANARY) val states = needed.map { SpeechModels.state(it).collectAsState().value } val ready = states.all { it == SpeechModels.State.Ready } val label = when { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/VoiceModelViews.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/VoiceModelViews.kt index d33033d..da48810 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/VoiceModelViews.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/VoiceModelViews.kt @@ -38,7 +38,7 @@ internal fun VoiceModelSection() { SettingsSection(title = "Voice: live words") { Text( "The voice assistant turns speech into text on this phone, and nothing you say leaves it. " + - "It needs two models, about 700 MB together, downloaded once. Use Wi-Fi if you can. " + + "It needs two models, about 270 MB together, downloaded once. " + "This one writes your words as you speak.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,