feat(chat): support non-Google speech-to-text providers (fixes #1038) (#1042)

This commit is contained in:
M57 authored and GitHub committed 2026-09-02 12:31:42 +03:00
1 parent 4668d482ef
commit dbd7008ee3
4 files changed
+173 -27

No files matched your search

@@ -2,12 +2,12 @@ package com.m57.hermescontrol.ui.chat
import android.Manifest
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.OpenableColumns
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -343,21 +343,18 @@ fun ChatScreen(
) { granted ->
ExternalActivityLifecycleGuard.externalActivityReturned()
if (granted) {
if (SpeechRecognizer.isRecognitionAvailable(context)) {
val intent =
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
)
putExtra(
RecognizerIntent.EXTRA_PROMPT,
micListeningPrompt,
)
}
if (SpeechInputHelper.isSpeechInputAvailable(context)) {
val intent = SpeechInputHelper.createSpeechIntent(micListeningPrompt)
isListening = true
launchExternalActivity {
speechLauncher.launch(intent)
try {
speechLauncher.launch(intent)
} catch (_: ActivityNotFoundException) {
isListening = false
scrollScope.launch {
snackbarHostState.showSnackbar(sttNotAvailableMsg)
}
}
}
} else {
scrollScope.launch {
@@ -781,21 +778,18 @@ fun ChatScreen(
Manifest.permission.RECORD_AUDIO,
) == PackageManager.PERMISSION_GRANTED
) {
if (SpeechRecognizer.isRecognitionAvailable(context)) {
val intent =
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
)
putExtra(
RecognizerIntent.EXTRA_PROMPT,
micListeningPrompt,
)
}
if (SpeechInputHelper.isSpeechInputAvailable(context)) {
val intent = SpeechInputHelper.createSpeechIntent(micListeningPrompt)
isListening = true
launchExternalActivity {
speechLauncher.launch(intent)
try {
speechLauncher.launch(intent)
} catch (_: ActivityNotFoundException) {
isListening = false
scrollScope.launch {
snackbarHostState.showSnackbar(sttNotAvailableMsg)
}
}
}
} else {
scrollScope.launch {
@@ -0,0 +1,54 @@
package com.m57.hermescontrol.ui.chat
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
object SpeechInputHelper {
/**
* Checks if speech recognition is available on this device through any registered provider,
* including standard Android [SpeechRecognizer] services or offline/third-party apps
* that handle [RecognizerIntent.ACTION_RECOGNIZE_SPEECH] (e.g. FUTO Voice Input).
*/
fun isSpeechInputAvailable(
context: Context,
isRecognizerServiceAvailable: (Context) -> Boolean = { ctx ->
runCatching { SpeechRecognizer.isRecognitionAvailable(ctx) }.getOrDefault(false)
},
): Boolean {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
val activities =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
runCatching {
context.packageManager.queryIntentActivities(
intent,
PackageManager.ResolveInfoFlags.of(0L),
)
}.getOrDefault(emptyList())
} else {
@Suppress("DEPRECATION")
runCatching {
context.packageManager.queryIntentActivities(intent, 0)
}.getOrDefault(emptyList())
}
return activities.isNotEmpty() || isRecognizerServiceAvailable(context)
}
/**
* Builds the standard speech recognition intent with language model and prompt.
*/
fun createSpeechIntent(prompt: String): Intent =
Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
)
putExtra(
RecognizerIntent.EXTRA_PROMPT,
prompt,
)
}
}