Skip to content
Merged
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 @@ -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

Expand Down Expand Up @@ -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 ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
新しく導入された while (isActive) ループ内で speech.startListening(...).collect に例外処理がありません。SpeechRecognizerManager.startListeningnewRecognizer.startListening(intent) のみ try/catch しており、SpeechRecognizer.createSpeechRecognizer の失敗やコールバック内の想定外例外がフローから送出されると、collect を抜けてそのまま scope.launch のコルーチンが異常終了します。scopeCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)CoroutineExceptionHandler を持たないため、未捕捉例外はアプリをクラッシュさせ、assistantState が LISTENING/THINKING のまま残るなどセッション状態が不正になります。ループ内で try/catch(または flow.catch)により showError を呼んでループを break するようにしてください。

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[other · low]
REPORT 分岐では showError のみを呼び、直前の partialText.value がクリアされません。同じ書き換えの中で RESTART 分岐では明示的に partialText.value = "" としているため、エラー表示時も同様にクリアし、UI に古い部分認識テキストが残るのを防ぐとよいです。

Suggestion:

Suggested change
VoiceDictationOutcome.REPORT -> showError(result.message)
VoiceDictationOutcome.REPORT -> {
partialText.value = ""
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
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading