added local implementation of speech_to_text_dialog
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.weblibre.speech_to_text_dialog">
|
||||
</manifest>
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.weblibre.speech_to_text_dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.speech.RecognizerIntent
|
||||
import androidx.annotation.NonNull
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextApi as PigeonSpeechToTextApi
|
||||
import eu.weblibre.speech_to_text_dialog.pigeons.SpeechToTextEvents
|
||||
import java.util.Locale
|
||||
|
||||
/// Plugin implementation for speech recognition dialog.
|
||||
///
|
||||
/// This plugin integrates Android's RecognizerIntent to provide
|
||||
/// speech-to-text functionality to Flutter apps.
|
||||
class SpeechToTextDialogPlugin : FlutterPlugin, ActivityAware,
|
||||
PluginRegistry.ActivityResultListener {
|
||||
|
||||
companion object {
|
||||
private const val REQ_CODE_SPEECH_INPUT = 120752
|
||||
}
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var speechApiHost: SpeechApiHost? = null
|
||||
|
||||
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
speechApiHost = SpeechApiHost(binding.binaryMessenger, this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
speechApiHost?.release()
|
||||
speechApiHost = null
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(@NonNull binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(@NonNull binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == REQ_CODE_SPEECH_INPUT) {
|
||||
speechApiHost?.handleActivityResult(resultCode, data)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Show the speech recognition dialog.
|
||||
///
|
||||
/// @param locale The locale string (e.g., "en-US", "de-DE") or null for device default.
|
||||
/// @return true if the dialog was shown successfully, false otherwise.
|
||||
fun showSpeechDialog(locale: String?): Boolean {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
|
||||
)
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE,
|
||||
locale?.let { Locale(it) } ?: Locale.getDefault()
|
||||
)
|
||||
putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak")
|
||||
}
|
||||
|
||||
return try {
|
||||
activity?.startActivityForResult(intent, REQ_CODE_SPEECH_INPUT)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Host API implementation that handles Flutter-to-Android calls.
|
||||
class SpeechApiHost(
|
||||
private val messenger: BinaryMessenger,
|
||||
private val plugin: SpeechToTextDialogPlugin
|
||||
) {
|
||||
private val events: SpeechToTextEvents = SpeechToTextEvents(messenger)
|
||||
|
||||
init {
|
||||
// Set up the API handler
|
||||
PigeonSpeechToTextApi.setUp(messenger, object : PigeonSpeechToTextApi {
|
||||
override fun showDialog(locale: String?): Boolean {
|
||||
return plugin.showSpeechDialog(locale)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle the result from the speech recognition activity.
|
||||
fun handleActivityResult(resultCode: Int, data: Intent?) {
|
||||
sendResult(resultCode, data)
|
||||
}
|
||||
|
||||
private fun sendResult(resultCode: Int, data: Intent?) {
|
||||
val text = if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||||
if (!result.isNullOrEmpty()) {
|
||||
result[0]
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else {
|
||||
// User cancelled or recognition failed
|
||||
""
|
||||
}
|
||||
|
||||
events.onTextReceived(text) { }
|
||||
}
|
||||
|
||||
fun release() {
|
||||
PigeonSpeechToTextApi.setUp(messenger, null)
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
package eu.weblibre.speech_to_text_dialog.pigeons
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.BasicMessageChannel
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MessageCodec
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
private object SpeechToTextPigeonUtils {
|
||||
|
||||
fun createConnectionError(channelName: String): FlutterError {
|
||||
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
|
||||
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||
* @property code The error code.
|
||||
* @property message The error message.
|
||||
* @property details The error details. Must be a datatype supported by the api codec.
|
||||
*/
|
||||
class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
private open class SpeechToTextPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return super.readValueOfType(type, buffer)
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Host API - methods called from Flutter to native Android.
|
||||
*
|
||||
* Generated interface from Pigeon that represents a handler of messages from Flutter.
|
||||
*/
|
||||
interface SpeechToTextApi {
|
||||
/**
|
||||
* Show the speech recognition dialog.
|
||||
*
|
||||
* Returns [true] if the dialog was shown successfully.
|
||||
* Returns [false] if the speech recognition service is not available.
|
||||
*
|
||||
* The [locale] parameter specifies the language locale for recognition
|
||||
* (e.g., 'en-US', 'de-DE'). If null, uses the device default.
|
||||
*/
|
||||
fun showDialog(locale: String?): Boolean
|
||||
|
||||
companion object {
|
||||
/** The codec used by SpeechToTextApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
SpeechToTextPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `SpeechToTextApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: SpeechToTextApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val localeArg = args[0] as String?
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.showDialog(localeArg))
|
||||
} catch (exception: Throwable) {
|
||||
SpeechToTextPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Flutter API - callbacks from native Android to Flutter.
|
||||
*
|
||||
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
|
||||
*/
|
||||
class SpeechToTextEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||
companion object {
|
||||
/** The codec used by SpeechToTextEvents. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
SpeechToTextPigeonCodec()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when speech recognition completes with the recognized text.
|
||||
*
|
||||
* [text] contains the recognized speech text. May be empty if
|
||||
* recognition failed or was cancelled.
|
||||
*/
|
||||
fun onTextReceived(textArg: String, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(textArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(SpeechToTextPigeonUtils.createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user