diff --git a/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/AndCodeVoiceSession.kt b/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/AndCodeVoiceSession.kt index 7e9ecd80..62f23981 100644 --- a/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/AndCodeVoiceSession.kt +++ b/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/AndCodeVoiceSession.kt @@ -66,6 +66,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.UUID @@ -312,24 +313,53 @@ class AndCodeVoiceSession(context: Context) : listeningJob = scope.launch { - speech.startListening(language = speechLanguageTag(context)).collect { result -> - when (result) { - SpeechResult.Ready, - SpeechResult.Listening, - -> assistantState.value = VoiceState.LISTENING - SpeechResult.Processing -> assistantState.value = VoiceState.THINKING - is SpeechResult.PartialResult -> partialText.value = result.text - is SpeechResult.Result -> { - userText.value = result.text - partialText.value = "" - sendToOpenCode(result.text) + var silentSegments = 0 + while (isActive) { + var outcome = VoiceDictationOutcome.REPORT + speech.startListening(language = speechLanguageTag(context)).collect { result -> + when (result) { + SpeechResult.Ready, + SpeechResult.Listening, + -> assistantState.value = VoiceState.LISTENING + SpeechResult.Processing -> assistantState.value = VoiceState.THINKING + is SpeechResult.PartialResult -> partialText.value = result.text + is SpeechResult.Result -> { + silentSegments = 0 + submitRecognizedText(result.text) + outcome = VoiceDictationOutcome.FINISH + } + is SpeechResult.Error -> { + // Some recognisers deliver a partial utterance followed by an + // empty final bundle. Keep that utterance instead of discarding it. + val partial = partialText.value.trim() + outcome = + VoiceDictationPolicy.outcomeFor( + code = result.code, + hasTranscript = partial.isNotBlank(), + consecutiveFailures = silentSegments + 1, + ) + when (outcome) { + VoiceDictationOutcome.RESTART -> silentSegments++ + VoiceDictationOutcome.FINISH -> submitRecognizedText(partial) + VoiceDictationOutcome.REPORT -> showError(result.message) + } + } } - is SpeechResult.Error -> showError(result.message) } + if (outcome != VoiceDictationOutcome.RESTART) break + partialText.value = "" + assistantState.value = VoiceState.LISTENING + delay(SPEECH_RESTART_DELAY_MS) } } } + private fun submitRecognizedText(text: String) { + userText.value = text + partialText.value = "" + sendToOpenCode(text) + } + private fun sendToOpenCode(text: String) { val activeBackend = backend ?: return assistantState.value = VoiceState.THINKING @@ -446,6 +476,8 @@ private enum class VoiceState { ERROR, } +private const val SPEECH_RESTART_DELAY_MS = 200L + private fun speechLanguageTag(context: Context): String { val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/SpeechRecognizerManager.kt b/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/SpeechRecognizerManager.kt index 3526b6e9..25bf3cce 100644 --- a/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/SpeechRecognizerManager.kt +++ b/app/src/main/java/com/yugahashimoto/andcode/feature/assistant/SpeechRecognizerManager.kt @@ -138,7 +138,6 @@ class SpeechRecognizerManager(private val context: Context) { putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) putExtra(RecognizerIntent.EXTRA_LANGUAGE, language) putExtra(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, language) - putExtra(RecognizerIntent.EXTRA_ONLY_RETURN_LANGUAGE_PREFERENCE, language) putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true) putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, MAX_RESULTS) putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, SILENCE_LENGTH_MS) diff --git a/app/src/test/java/com/yugahashimoto/andcode/feature/assistant/VoiceDictationPolicyTest.kt b/app/src/test/java/com/yugahashimoto/andcode/feature/assistant/VoiceDictationPolicyTest.kt index eb44588d..8dbd8c9c 100644 --- a/app/src/test/java/com/yugahashimoto/andcode/feature/assistant/VoiceDictationPolicyTest.kt +++ b/app/src/test/java/com/yugahashimoto/andcode/feature/assistant/VoiceDictationPolicyTest.kt @@ -18,9 +18,9 @@ class VoiceDictationPolicyTest { } @Test - fun `silence after something was dictated ends the dictation without an error`() { - // The user stopped talking. Their words are in the composer, so a red "could not - // recognise" banner would be both wrong and alarming. + fun `a partial result followed by silence finishes without an error`() { + // The voice session submits the partial transcript when the final callback is empty, so a + // red "could not recognise" banner would be both wrong and alarming. assertEquals( VoiceDictationOutcome.FINISH, VoiceDictationPolicy.outcomeFor(