10 changed files with 740 additions and 3 deletions
@ -1,5 +1,22 @@ |
|||
package com.example.deep_voice |
|||
|
|||
import com.ryanheise.audioservice.AudioServiceActivity |
|||
import io.flutter.embedding.engine.FlutterEngine |
|||
|
|||
class MainActivity: AudioServiceActivity() |
|||
class MainActivity: AudioServiceActivity() { |
|||
private var volcSpeechBridge: VolcSpeechBridge? = null |
|||
|
|||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { |
|||
super.configureFlutterEngine(flutterEngine) |
|||
|
|||
// 初始化火山语音识别桥接类 |
|||
volcSpeechBridge = VolcSpeechBridge(this, flutterEngine.dartExecutor.binaryMessenger) |
|||
} |
|||
|
|||
override fun onDestroy() { |
|||
// 释放火山语音识别资源 |
|||
volcSpeechBridge?.dispose() |
|||
volcSpeechBridge = null |
|||
super.onDestroy() |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,293 @@ |
|||
package com.example.deep_voice |
|||
|
|||
import android.app.Activity |
|||
import android.content.Context |
|||
import android.util.Log |
|||
import com.bytedance.speech.speechengine.SpeechEngine |
|||
import com.bytedance.speech.speechengine.SpeechEngineDefines |
|||
import com.bytedance.speech.speechengine.SpeechEngineGenerator |
|||
import io.flutter.plugin.common.BinaryMessenger |
|||
import io.flutter.plugin.common.EventChannel |
|||
import io.flutter.plugin.common.MethodChannel |
|||
import io.flutter.plugin.common.MethodChannel.Result |
|||
import org.json.JSONObject |
|||
|
|||
|
|||
|
|||
/** |
|||
* 火山语音识别桥接类 |
|||
* 负责与火山语音识别 SDK 交互,并将结果通过 MethodChannel 和 EventChannel 传递给 Flutter 层 |
|||
*/ |
|||
class VolcSpeechBridge( |
|||
private val activity: Activity, |
|||
messenger: BinaryMessenger |
|||
) : SpeechEngine.SpeechListener { |
|||
|
|||
companion object { |
|||
private const val TAG = "VolcSpeechBridge" |
|||
private const val CHANNEL_NAME = "com.example.deep_voice/speech_recognition" |
|||
private const val EVENT_CHANNEL_NAME = "com.example.deep_voice/speech_recognition_events" |
|||
|
|||
// 定义指令常量 |
|||
private const val DIRECTIVE_START_RECOGNITION = 1 |
|||
private const val DIRECTIVE_STOP_RECOGNITION = 2 |
|||
} |
|||
|
|||
private var engine: SpeechEngine? = null |
|||
private var methodResult: Result? = null |
|||
private var eventSink: EventChannel.EventSink? = null |
|||
private var isInitialized = false |
|||
|
|||
// 初始化 MethodChannel 和 EventChannel |
|||
init { |
|||
// 设置 MethodChannel 处理方法调用 |
|||
MethodChannel(messenger, CHANNEL_NAME).setMethodCallHandler { call, result -> |
|||
when (call.method) { |
|||
"initialize" -> { |
|||
val appKey = call.argument<String>("appKey") ?: "" |
|||
val accessKey = call.argument<String>("accessKey") ?: "" |
|||
initialize(appKey, accessKey, result) |
|||
} |
|||
"startRecognition" -> { |
|||
startRecognition(result) |
|||
} |
|||
"stopRecognition" -> { |
|||
stopRecognition(result) |
|||
} |
|||
"isInitialized" -> { |
|||
result.success(isInitialized) |
|||
} |
|||
else -> { |
|||
result.notImplemented() |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 设置 EventChannel 处理事件流 |
|||
EventChannel(messenger, EVENT_CHANNEL_NAME).setStreamHandler(object : EventChannel.StreamHandler { |
|||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { |
|||
eventSink = events |
|||
} |
|||
|
|||
override fun onCancel(arguments: Any?) { |
|||
eventSink = null |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 初始化火山语音识别引擎 |
|||
*/ |
|||
fun initialize(appKey: String, accessKey: String, result: Result) { |
|||
methodResult = result |
|||
try { |
|||
Log.i(TAG, "Initializing Volcengine Speech Engine") |
|||
|
|||
// 创建引擎实例 |
|||
engine = SpeechEngineGenerator.getInstance() |
|||
engine?.createEngine() |
|||
|
|||
// 设置监听器 |
|||
engine?.setListener(this) |
|||
|
|||
// 设置应用 ID 和访问密钥 |
|||
engine?.setOptionString("app_id", appKey) |
|||
engine?.setOptionString("access_key", accessKey) |
|||
|
|||
// 设置语音识别参数 |
|||
engine?.setOptionBoolean("enable_punctuation", true) // 启用标点符号 |
|||
engine?.setOptionBoolean("enable_intermediate_result", true) // 启用中间结果 |
|||
engine?.setOptionBoolean("enable_vad", true) // 启用语音活动检测 |
|||
|
|||
// 设置语言(普通话) |
|||
engine?.setOptionString("language", "zh") |
|||
|
|||
// 初始化引擎 |
|||
val ret = engine?.initEngine() |
|||
if (ret == SpeechEngineDefines.ERR_NO_ERROR) { |
|||
isInitialized = true |
|||
Log.i(TAG, "Volcengine Speech Engine initialized successfully") |
|||
result.success("Engine initialized") |
|||
|
|||
// 发送状态更新到 Flutter |
|||
sendEvent("status", "initialized") |
|||
} else { |
|||
Log.e(TAG, "Engine initialization failed with error code: $ret") |
|||
result.error("INIT_ERROR", "Engine initialization failed with error code: $ret", null) |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Initialization failed", e) |
|||
result.error("INIT_ERROR", e.message, null) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 启动语音识别 |
|||
*/ |
|||
fun startRecognition(result: Result) { |
|||
try { |
|||
if (!isInitialized) { |
|||
result.error("NOT_INITIALIZED", "Speech engine is not initialized", null) |
|||
return |
|||
} |
|||
|
|||
Log.i(TAG, "Starting speech recognition") |
|||
val ret = engine?.sendDirective(DIRECTIVE_START_RECOGNITION, "") |
|||
|
|||
if (ret == SpeechEngineDefines.ERR_NO_ERROR) { |
|||
result.success("Recognition started") |
|||
// 发送状态更新到 Flutter |
|||
sendEvent("status", "listening") |
|||
} else { |
|||
Log.e(TAG, "Start recognition failed with error code: $ret") |
|||
result.error("START_ERROR", "Start recognition failed with error code: $ret", null) |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Start recognition failed", e) |
|||
result.error("START_ERROR", e.message, null) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 停止语音识别 |
|||
*/ |
|||
fun stopRecognition(result: Result) { |
|||
try { |
|||
if (!isInitialized) { |
|||
result.error("NOT_INITIALIZED", "Speech engine is not initialized", null) |
|||
return |
|||
} |
|||
|
|||
Log.i(TAG, "Stopping speech recognition") |
|||
val ret = engine?.sendDirective(DIRECTIVE_STOP_RECOGNITION, "") |
|||
|
|||
if (ret == SpeechEngineDefines.ERR_NO_ERROR) { |
|||
result.success("Recognition stopped") |
|||
// 发送状态更新到 Flutter |
|||
sendEvent("status", "idle") |
|||
} else { |
|||
Log.e(TAG, "Stop recognition failed with error code: $ret") |
|||
result.error("STOP_ERROR", "Stop recognition failed with error code: $ret", null) |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Stop recognition failed", e) |
|||
result.error("STOP_ERROR", e.message, null) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 释放资源 |
|||
*/ |
|||
fun dispose() { |
|||
try { |
|||
engine?.destroyEngine() |
|||
engine = null |
|||
isInitialized = false |
|||
Log.i(TAG, "Volcengine Speech Engine released") |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Error releasing engine", e) |
|||
} |
|||
} |
|||
|
|||
// ------------------ |
|||
// 发送事件到 Flutter |
|||
// ------------------ |
|||
|
|||
private fun sendEvent(eventName: String, data: Any) { |
|||
try { |
|||
val eventData = JSONObject().apply { |
|||
put("event", eventName) |
|||
put("data", data) |
|||
} |
|||
|
|||
eventSink?.success(eventData.toString()) |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Error sending event", e) |
|||
} |
|||
} |
|||
|
|||
// ------------------ |
|||
// SpeechListener 接口回调 |
|||
// ------------------ |
|||
|
|||
/** |
|||
* 处理语音消息回调 |
|||
* 这是 SpeechListener 接口的必须实现方法 |
|||
*/ |
|||
override fun onSpeechMessage(messageType: Int, messageData: ByteArray?, messageLength: Int) { |
|||
try { |
|||
when (messageType) { |
|||
// 初始化完成消息 |
|||
1 -> { |
|||
Log.i(TAG, "Engine initialized successfully") |
|||
isInitialized = true |
|||
|
|||
// 通知 Flutter 初始化完成 |
|||
methodResult?.success("Engine initialized") |
|||
methodResult = null |
|||
|
|||
// 发送状态更新到 Flutter |
|||
sendEvent("status", "initialized") |
|||
} |
|||
|
|||
// 错误消息 |
|||
2 -> { |
|||
val errorCode = messageData?.let { String(it, 0, messageLength).toIntOrNull() } ?: -1 |
|||
Log.e(TAG, "Engine error: $errorCode") |
|||
|
|||
// 通知 Flutter 识别出错 |
|||
val errorData = JSONObject().apply { |
|||
put("code", errorCode) |
|||
put("message", "Error code: $errorCode") |
|||
} |
|||
|
|||
sendEvent("error", errorData.toString()) |
|||
} |
|||
|
|||
// VAD 状态变化消息 |
|||
3 -> { |
|||
val state = messageData?.let { String(it, 0, messageLength).toIntOrNull() } ?: -1 |
|||
Log.i(TAG, "VAD state changed: $state") |
|||
|
|||
val vadState = when (state) { |
|||
0 -> "silence" |
|||
1 -> "speech" |
|||
else -> "unknown" |
|||
} |
|||
|
|||
sendEvent("vad", vadState) |
|||
} |
|||
|
|||
// 识别结果消息 |
|||
4 -> { |
|||
messageData?.let { |
|||
val resultJson = String(it, 0, messageLength) |
|||
try { |
|||
val jsonObject = JSONObject(resultJson) |
|||
val text = jsonObject.optString("text", "") |
|||
val isFinal = jsonObject.optBoolean("is_final", false) |
|||
|
|||
Log.i(TAG, "Recognition result: $text, isFinal: $isFinal") |
|||
|
|||
// 发送识别结果到 Flutter |
|||
val resultData = JSONObject().apply { |
|||
put("text", text) |
|||
put("isFinal", isFinal) |
|||
} |
|||
|
|||
sendEvent("result", resultData.toString()) |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Error parsing result JSON", e) |
|||
} |
|||
} |
|||
} |
|||
|
|||
else -> { |
|||
Log.d(TAG, "Received unknown message type: $messageType") |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Error processing speech message", e) |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,264 @@ |
|||
import 'dart:async'; |
|||
import 'dart:convert'; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter/foundation.dart'; |
|||
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|||
|
|||
/// 火山语音识别服务 |
|||
/// 负责与原生层的火山语音识别引擎交互 |
|||
class VoiceRecognitionService extends GetxService { |
|||
// 方法通道,用于调用原生方法 |
|||
static const MethodChannel _methodChannel = |
|||
MethodChannel('com.example.deep_voice/speech_recognition'); |
|||
|
|||
// 事件通道,用于接收原生层的事件 |
|||
static const EventChannel _eventChannel = |
|||
EventChannel('com.example.deep_voice/speech_recognition_events'); |
|||
|
|||
// 状态变量 |
|||
final isInitialized = false.obs; |
|||
final isListening = false.obs; |
|||
final isConnecting = true.obs; |
|||
|
|||
// 识别结果 |
|||
final recognizedText = ''.obs; |
|||
|
|||
// 回调函数 |
|||
Function(String)? onRecognitionResult; |
|||
Function(String)? onError; |
|||
Function()? onRecognitionComplete; |
|||
Function(String)? onConnectionStatusChanged; |
|||
Function(String)? onSentenceComplete; |
|||
|
|||
// 事件流订阅 |
|||
StreamSubscription? _eventSubscription; |
|||
|
|||
// 火山语音识别的 AppKey 和 AccessKey |
|||
late final String _appKey; |
|||
late final String _accessKey; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_loadCredentials(); |
|||
_setupEventListener(); |
|||
} |
|||
|
|||
/// 加载火山语音识别的凭证 |
|||
void _loadCredentials() { |
|||
try { |
|||
_appKey = dotenv.env['VOLC_SPEECH_APP_KEY'] ?? ''; |
|||
_accessKey = dotenv.env['VOLC_SPEECH_ACCESS_KEY'] ?? ''; |
|||
|
|||
if (_appKey.isEmpty || _accessKey.isEmpty) { |
|||
debugPrint('警告: 火山语音识别凭证未设置,请在 .env 文件中设置 VOLC_SPEECH_APP_KEY 和 VOLC_SPEECH_ACCESS_KEY'); |
|||
} |
|||
} catch (e) { |
|||
debugPrint('加载火山语音识别凭证失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 设置事件监听器 |
|||
void _setupEventListener() { |
|||
try { |
|||
_eventSubscription = _eventChannel.receiveBroadcastStream().listen( |
|||
(dynamic event) { |
|||
_handleEvent(event.toString()); |
|||
}, |
|||
onError: (dynamic error) { |
|||
debugPrint('事件通道错误: $error'); |
|||
if (onError != null) { |
|||
onError!('事件通道错误: $error'); |
|||
} |
|||
}, |
|||
); |
|||
} catch (e) { |
|||
debugPrint('设置事件监听器失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 处理来自原生层的事件 |
|||
void _handleEvent(String eventJson) { |
|||
try { |
|||
final eventData = jsonDecode(eventJson); |
|||
final eventName = eventData['event']; |
|||
final data = eventData['data']; |
|||
|
|||
switch (eventName) { |
|||
case 'status': |
|||
_handleStatusEvent(data.toString()); |
|||
break; |
|||
case 'result': |
|||
_handleResultEvent(data.toString()); |
|||
break; |
|||
case 'error': |
|||
_handleErrorEvent(data.toString()); |
|||
break; |
|||
case 'vad': |
|||
_handleVadEvent(data.toString()); |
|||
break; |
|||
} |
|||
} catch (e) { |
|||
debugPrint('处理事件失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 处理状态事件 |
|||
void _handleStatusEvent(String status) { |
|||
switch (status) { |
|||
case 'initialized': |
|||
isInitialized.value = true; |
|||
isConnecting.value = false; |
|||
break; |
|||
case 'listening': |
|||
isListening.value = true; |
|||
break; |
|||
case 'idle': |
|||
isListening.value = false; |
|||
break; |
|||
} |
|||
|
|||
if (onConnectionStatusChanged != null) { |
|||
onConnectionStatusChanged!(status); |
|||
} |
|||
} |
|||
|
|||
/// 处理识别结果事件 |
|||
void _handleResultEvent(String resultJson) { |
|||
try { |
|||
final resultData = jsonDecode(resultJson); |
|||
final text = resultData['text']; |
|||
final isFinal = resultData['isFinal']; |
|||
|
|||
// 更新识别文本 |
|||
recognizedText.value = text; |
|||
|
|||
// 调用回调函数 |
|||
if (onRecognitionResult != null) { |
|||
onRecognitionResult!(text); |
|||
} |
|||
|
|||
// 如果是最终结果,调用完成回调 |
|||
if (isFinal && text.isNotEmpty) { |
|||
if (onSentenceComplete != null) { |
|||
onSentenceComplete!(text); |
|||
} |
|||
|
|||
if (onRecognitionComplete != null) { |
|||
onRecognitionComplete!(); |
|||
} |
|||
} |
|||
} catch (e) { |
|||
debugPrint('处理识别结果失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 处理错误事件 |
|||
void _handleErrorEvent(String errorJson) { |
|||
try { |
|||
final errorData = jsonDecode(errorJson); |
|||
final errorMessage = errorData['message']; |
|||
|
|||
debugPrint('语音识别错误: $errorMessage'); |
|||
|
|||
if (onError != null) { |
|||
onError!(errorMessage); |
|||
} |
|||
} catch (e) { |
|||
debugPrint('处理错误事件失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 处理 VAD 事件 |
|||
void _handleVadEvent(String vadState) { |
|||
// 可以根据需要处理 VAD 状态变化 |
|||
debugPrint('VAD 状态: $vadState'); |
|||
} |
|||
|
|||
/// 初始化语音识别引擎 |
|||
Future<void> initialize() async { |
|||
try { |
|||
isConnecting.value = true; |
|||
|
|||
// 检查凭证 |
|||
if (_appKey.isEmpty || _accessKey.isEmpty) { |
|||
throw '火山语音识别凭证未设置'; |
|||
} |
|||
|
|||
// 调用原生方法初始化引擎 |
|||
final result = await _methodChannel.invokeMethod<String>( |
|||
'initialize', |
|||
{ |
|||
'appKey': _appKey, |
|||
'accessKey': _accessKey, |
|||
}, |
|||
); |
|||
|
|||
debugPrint('初始化结果: $result'); |
|||
|
|||
// 初始化成功后,状态会通过事件通道更新 |
|||
} catch (e) { |
|||
isConnecting.value = false; |
|||
debugPrint('初始化语音识别引擎失败: $e'); |
|||
if (onError != null) { |
|||
onError!('初始化失败: $e'); |
|||
} |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
/// 开始语音识别 |
|||
Future<void> startRecognition() async { |
|||
try { |
|||
// 检查是否已初始化 |
|||
if (!isInitialized.value) { |
|||
await initialize(); |
|||
} |
|||
|
|||
// 调用原生方法开始识别 |
|||
final result = await _methodChannel.invokeMethod<String>('startRecognition'); |
|||
debugPrint('开始识别结果: $result'); |
|||
|
|||
// 状态会通过事件通道更新 |
|||
} catch (e) { |
|||
debugPrint('开始语音识别失败: $e'); |
|||
if (onError != null) { |
|||
onError!('开始识别失败: $e'); |
|||
} |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
/// 停止语音识别 |
|||
Future<void> stopRecognition() async { |
|||
try { |
|||
// 检查是否已初始化 |
|||
if (!isInitialized.value) { |
|||
return; |
|||
} |
|||
|
|||
// 调用原生方法停止识别 |
|||
final result = await _methodChannel.invokeMethod<String>('stopRecognition'); |
|||
debugPrint('停止识别结果: $result'); |
|||
|
|||
// 状态会通过事件通道更新 |
|||
} catch (e) { |
|||
debugPrint('停止语音识别失败: $e'); |
|||
if (onError != null) { |
|||
onError!('停止识别失败: $e'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
// 取消事件订阅 |
|||
_eventSubscription?.cancel(); |
|||
|
|||
// 停止识别 |
|||
stopRecognition(); |
|||
|
|||
super.onClose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../../data/services/voice_recognition_service.dart'; |
|||
|
|||
class SpeechDemoPage extends StatefulWidget { |
|||
const SpeechDemoPage({Key? key}) : super(key: key); |
|||
|
|||
@override |
|||
_SpeechDemoPageState createState() => _SpeechDemoPageState(); |
|||
} |
|||
|
|||
class _SpeechDemoPageState extends State<SpeechDemoPage> { |
|||
final VoiceRecognitionService _recognitionService = Get.find<VoiceRecognitionService>(); |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_initializeEngine(); |
|||
} |
|||
|
|||
Future<void> _initializeEngine() async { |
|||
try { |
|||
await _recognitionService.initialize(); |
|||
} catch (e) { |
|||
if (mounted) { |
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar(content: Text('初始化失败: $e')), |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> _startRecognition() async { |
|||
try { |
|||
await _recognitionService.startRecognition(); |
|||
} catch (e) { |
|||
if (mounted) { |
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar(content: Text('启动识别失败: $e')), |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Future<void> _stopRecognition() async { |
|||
try { |
|||
await _recognitionService.stopRecognition(); |
|||
} catch (e) { |
|||
if (mounted) { |
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar(content: Text('停止识别失败: $e')), |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: AppBar( |
|||
title: const Text('火山语音识别演示'), |
|||
), |
|||
body: Center( |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
// 状态显示 |
|||
Obx(() { |
|||
String status = '准备中'; |
|||
if (_recognitionService.isConnecting.value) { |
|||
status = '连接中...'; |
|||
} else if (_recognitionService.isInitialized.value) { |
|||
status = _recognitionService.isListening.value ? '正在听...' : '就绪'; |
|||
} |
|||
return Text('状态: $status', style: const TextStyle(fontSize: 18)); |
|||
}), |
|||
|
|||
const SizedBox(height: 20), |
|||
|
|||
// 识别结果显示 |
|||
Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Container( |
|||
width: double.infinity, |
|||
height: 200, |
|||
decoration: BoxDecoration( |
|||
border: Border.all(color: Colors.grey), |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
padding: const EdgeInsets.all(16), |
|||
child: Obx(() { |
|||
return SingleChildScrollView( |
|||
child: Text( |
|||
_recognitionService.recognizedText.value, |
|||
style: const TextStyle(fontSize: 16), |
|||
), |
|||
); |
|||
}), |
|||
), |
|||
), |
|||
|
|||
const SizedBox(height: 20), |
|||
|
|||
// 控制按钮 |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Obx(() { |
|||
return ElevatedButton( |
|||
onPressed: _recognitionService.isInitialized.value && !_recognitionService.isListening.value |
|||
? _startRecognition |
|||
: null, |
|||
child: const Text('开始识别'), |
|||
); |
|||
}), |
|||
|
|||
const SizedBox(width: 20), |
|||
|
|||
Obx(() { |
|||
return ElevatedButton( |
|||
onPressed: _recognitionService.isListening.value |
|||
? _stopRecognition |
|||
: null, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.red, |
|||
), |
|||
child: const Text('停止识别'), |
|||
); |
|||
}), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue