Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ object VoiceConstants {
const val VOICE_LANG_AUTO = "auto"
const val PREF_VOICE_CPU_THREADS = "voice_cpu_threads"
const val PREF_VOICE_CUSTOM_PROMPT = "voice_custom_prompt"
/** Transcribe the recording in pieces at speech pauses while still recording */
const val PREF_VOICE_TRANSCRIBE_WHILE_SPEAKING = "voice_transcribe_while_speaking"
const val PREF_VOICE_MIC_SENSITIVITY = "voice_mic_sensitivity"
const val PREF_VOICE_MAX_DURATION_SECONDS = "voice_max_duration_seconds"
const val PREF_VOICE_AUTO_RETRY = "pref_voice_auto_retry"
Expand Down
263 changes: 235 additions & 28 deletions app/src/main/java/helium314/keyboard/latin/voice/VoiceInputManager.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,35 @@ class VoicePluginManager(private val context: Context) : IBinder.DeathRecipient
return success
}

// The plugin is bound without foreground priority, which saves battery while it waits, but then Android
// gives it less CPU and recognition takes about twice as long. While dictating, a second binding with
// normal priority raises it (Android uses the higher one); it is removed when the dictation ends.
private var isBoosted = false
private val boostConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {}
override fun onServiceDisconnected(name: ComponentName?) {}
}

/** Normal priority for the plugin while [active] (recording and recognizing), see [boostConnection] */
fun setDictationPriority(active: Boolean) {
if (active == isBoosted) return
if (active) {
if (!isBound) return
isBoosted = try {
context.bindService(Intent().apply { component = resolveServiceComponent() }, boostConnection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
Log.w(TAG, "Could not raise the priority of the voice plugin", e)
false
}
} else {
isBoosted = false
try { context.unbindService(boostConnection) } catch (e: Exception) { }
}
}

fun unbind() {
cancelScheduledUnbind()
setDictationPriority(false)
if (isBound || isConnecting) {
try {
if (deathRecipientRegistered) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,20 @@ fun VoiceSettingsScreen(
}
}

val whileSpeakingSetting = remember(context) {
Setting(
key = VoiceConstants.PREF_VOICE_TRANSCRIBE_WHILE_SPEAKING,
title = context.getString(R.string.voice_transcribe_while_speaking_title),
description = context.getString(R.string.voice_transcribe_while_speaking_summary)
) {
SwitchPreference(
setting = it,
default = false,
icon = R.drawable.ic_settings_advanced
)
}
}

val customPromptSetting = remember(context) {
Setting(
key = VoiceConstants.PREF_VOICE_CUSTOM_PROMPT,
Expand Down Expand Up @@ -833,6 +847,7 @@ fun VoiceSettingsScreen(

if (isOfflineVoiceEnabled) {
cpuThreadsSetting.Preference()
whileSpeakingSetting.Preference()
}
customPromptSetting.Preference()
if (isOfflineVoiceEnabled) {
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1756,4 +1756,7 @@ Ermöglicht lokale GGUF / llama.cpp-Inferenz für Korrekturlesen und Umschreiben
<string name="voice_max_duration_10m">10 Minuten</string>
<string name="text_expander_prefix_autocomplete_title">Verknüpfungen zur automatischen Vervollständigung</string>
<string name="text_expander_prefix_autocomplete_summary">Schlagen Sie Teilübereinstimmungen für Text- und Bildverknüpfungen ohne Präfix vor, nachdem Sie mehr als zwei Zeichen eingegeben haben (Verknüpfungen mit Präfix werden auch automatisch vervollständigt).</string>
<string name="voice_transcribe_while_speaking_title">Schon beim Sprechen erkennen</string>
<string name="voice_transcribe_while_speaking_summary">Bei jeder Sprechpause wird das bisher Gesagte schon erkannt. Nach dem Stopp bleibt nur das letzte Stück: viel kürzeres Warten bei langen Diktaten.</string>
<string name="voice_piecewise_field_changed">Diktat beendet: Das Textfeld hat gewechselt, der Rest wurde nicht eingefügt</string>
</resources>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2455,4 +2455,7 @@ New dictionary:
<string name="style_plain">Normal</string>
<string name="style_active_indicator">Active style: %s</string>
<string name="style_active_none">None (Plain text)</string>
<string name="voice_transcribe_while_speaking_title">Transcribe while speaking</string>
<string name="voice_transcribe_while_speaking_summary">At every pause in speech the part spoken so far is already transcribed. After stopping, only the last part is left: much shorter wait for long dictations.</string>
<string name="voice_piecewise_field_changed">Dictation stopped: the text field changed, the rest was not inserted</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin.voice

import org.junit.Assert.assertEquals
import org.junit.Test

class VoiceProcessingTimeoutTest {
@Test
fun longerRecordingsGetMoreTime() {
assertEquals(60_000L, VoiceInputManager.processingTimeoutFor(0L)) // at least a minute
assertEquals(60_000L, VoiceInputManager.processingTimeoutFor(10_000L))
assertEquals(195_000L, VoiceInputManager.processingTimeoutFor(60_000L)) // 15 s + 3 min
assertEquals(60_000L, VoiceInputManager.processingTimeoutFor(-5L))
}
}
Loading