You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
481 lines
14 KiB
481 lines
14 KiB
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
|
import 'package:speech_to_text/speech_recognition_result.dart';
|
|
import 'package:speech_to_text/speech_recognition_error.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
import '../asr_service.dart';
|
|
|
|
/// ASR状态枚举
|
|
enum AsrState {
|
|
/// 未初始化
|
|
notInitialized,
|
|
|
|
/// 已初始化但未开始监听
|
|
initialized,
|
|
|
|
/// 正在监听
|
|
listening,
|
|
|
|
/// 正在处理识别结果
|
|
processing,
|
|
|
|
/// 识别完成
|
|
done,
|
|
|
|
/// 发生错误
|
|
error,
|
|
}
|
|
|
|
/// Flutter ASR服务,负责本地语音转文本功能
|
|
/// 使用speech_to_text插件实现语音识别功能
|
|
class FlutterAsrService extends GetxService implements AsrService {
|
|
static final FlutterAsrService to = Get.put(FlutterAsrService());
|
|
|
|
// speech_to_text 实例
|
|
final stt.SpeechToText _speech = stt.SpeechToText();
|
|
|
|
// 可用的语音识别语言
|
|
final List<String> _supportedLanguages = [];
|
|
@override
|
|
List<String> get supportedLanguages => _supportedLanguages;
|
|
|
|
// 当前选择的语言
|
|
final RxString _currentLocale = ''.obs;
|
|
String get currentLocale => _currentLocale.value;
|
|
|
|
// 是否可用
|
|
final RxBool _isAvailable = false.obs;
|
|
bool get isAvailable => _isAvailable.value;
|
|
|
|
// 是否正在监听
|
|
bool _isListening = false;
|
|
|
|
// 当前ASR状态
|
|
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
|
|
AsrState get asrState => _asrState.value;
|
|
|
|
// 连续识别模式的结果流控制器
|
|
StreamController<RecognitionEvent>? _continuousRecognitionController;
|
|
|
|
FlutterAsrService() {
|
|
_initSpeech();
|
|
}
|
|
|
|
/// 初始化语音识别
|
|
Future<void> _initSpeech() async {
|
|
try {
|
|
_asrState.value = AsrState.notInitialized;
|
|
|
|
// 初始化语音识别,使用较短的最终超时时间,提高响应速度
|
|
_isAvailable.value = await _speech.initialize(
|
|
onStatus: _onSpeechStatus,
|
|
onError: _onSpeechError,
|
|
debugLogging: false,
|
|
finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回
|
|
);
|
|
|
|
if (_isAvailable.value) {
|
|
// 获取支持的语言列表
|
|
final systemLocales = await _speech.locales();
|
|
|
|
// 清空并重新填充支持的语言列表
|
|
_supportedLanguages.clear();
|
|
for (var locale in systemLocales) {
|
|
if (!_supportedLanguages.contains(locale.localeId)) {
|
|
_supportedLanguages.add(locale.localeId);
|
|
}
|
|
}
|
|
|
|
// 设置默认语言(优先使用系统语言或中文)
|
|
var systemLocale = await _speech.systemLocale();
|
|
if (systemLocale != null) {
|
|
_currentLocale.value = systemLocale.localeId;
|
|
} else if (_supportedLanguages.contains('zh-CN')) {
|
|
_currentLocale.value = 'zh-CN';
|
|
} else if (_supportedLanguages.isNotEmpty) {
|
|
_currentLocale.value = _supportedLanguages.first;
|
|
}
|
|
|
|
_asrState.value = AsrState.initialized;
|
|
Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言');
|
|
} else {
|
|
_asrState.value = AsrState.error;
|
|
Logger.error('语音识别不可用');
|
|
}
|
|
} catch (e) {
|
|
_isAvailable.value = false;
|
|
_asrState.value = AsrState.error;
|
|
Logger.error('语音识别初始化失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 处理语音识别状态变化
|
|
void _onSpeechStatus(String status) {
|
|
Logger.info('语音识别状态: $status');
|
|
|
|
switch (status) {
|
|
case 'listening':
|
|
_asrState.value = AsrState.listening;
|
|
_isListening = true;
|
|
break;
|
|
case 'notListening':
|
|
if (_asrState.value == AsrState.listening) {
|
|
_asrState.value = AsrState.processing;
|
|
}
|
|
_isListening = false;
|
|
break;
|
|
case 'done':
|
|
_asrState.value = AsrState.done;
|
|
_isListening = false;
|
|
|
|
// 在连续识别模式下,当一次识别完成后立即开始下一次识别
|
|
if (_continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) {
|
|
_restartContinuousRecognition();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// 处理语音识别错误
|
|
void _onSpeechError(SpeechRecognitionError error) {
|
|
Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})');
|
|
_asrState.value = AsrState.error;
|
|
|
|
// 向连续识别控制器发送错误事件(如果存在)
|
|
_continuousRecognitionController?.add(
|
|
RecognitionEvent.error(error.errorMsg)
|
|
);
|
|
|
|
// 在连续识别模式下,尝试从错误中恢复(非永久性错误)
|
|
if (!error.permanent && _continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) {
|
|
_restartContinuousRecognition();
|
|
}
|
|
}
|
|
|
|
/// 启动语音识别的统一方法
|
|
Future<bool> _startListening({
|
|
required Function(SpeechRecognitionResult) onResult,
|
|
Duration listenFor = const Duration(seconds: 30),
|
|
Duration pauseFor = const Duration(seconds: 3),
|
|
bool autoPunctuation = true,
|
|
}) async {
|
|
try {
|
|
// 如果当前正在监听,先停止
|
|
if (_speech.isListening) {
|
|
await _speech.stop();
|
|
// 延长等待时间,确保资源完全释放
|
|
await Future.delayed(const Duration(milliseconds: 800));
|
|
}
|
|
|
|
// 在启动前再次检查状态
|
|
if (_speech.isListening) {
|
|
Logger.warning('停止识别后,仍处于监听状态,可能有系统冲突');
|
|
await _speech.stop();
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
}
|
|
|
|
// 启动语音识别前确保状态正确
|
|
_asrState.value = AsrState.listening;
|
|
|
|
// 启动语音识别
|
|
final result = await _speech.listen(
|
|
onResult: onResult,
|
|
localeId: _currentLocale.value,
|
|
listenFor: listenFor,
|
|
pauseFor: pauseFor,
|
|
listenOptions: stt.SpeechListenOptions(
|
|
partialResults: true,
|
|
autoPunctuation: autoPunctuation,
|
|
// 使用独白模式,减少与系统交互冲突
|
|
listenMode: stt.ListenMode.dictation,
|
|
),
|
|
) ?? false;
|
|
|
|
if (!result) {
|
|
Logger.error('语音识别启动失败');
|
|
_asrState.value = AsrState.error;
|
|
}
|
|
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('启动语音识别失败: $e');
|
|
_asrState.value = AsrState.error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 重新启动连续识别
|
|
Future<void> _restartContinuousRecognition() async {
|
|
try {
|
|
// 确保控制器有效
|
|
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) {
|
|
Logger.info('连续识别控制器已关闭,跳过重启');
|
|
return;
|
|
}
|
|
|
|
// 检查系统状态,避免频繁重启
|
|
if (_speech.isListening) {
|
|
Logger.info('当前已在监听中,无需重启');
|
|
return;
|
|
}
|
|
|
|
// 延长等待时间,避免与系统资源冲突
|
|
await Future.delayed(const Duration(milliseconds: 1000));
|
|
|
|
// 再次检查状态
|
|
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) {
|
|
Logger.info('延迟期间控制器已关闭,跳过重启');
|
|
return;
|
|
}
|
|
|
|
Logger.info('尝试重新启动连续识别');
|
|
|
|
// 使用统一方法启动识别
|
|
final success = await _startListening(
|
|
onResult: _onSpeechResult,
|
|
// 增加超时时间和暂停时间
|
|
listenFor: const Duration(seconds: 30),
|
|
pauseFor: const Duration(seconds: 4),
|
|
);
|
|
|
|
if (success) {
|
|
Logger.info('连续识别重启成功');
|
|
} else {
|
|
Logger.error('连续识别重启失败');
|
|
_continuousRecognitionController?.add(
|
|
RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')
|
|
);
|
|
}
|
|
} catch (e) {
|
|
Logger.error('重启连续识别失败: $e');
|
|
|
|
// 发送错误给控制器
|
|
_continuousRecognitionController?.add(
|
|
RecognitionEvent.error('重启失败: $e')
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 处理语音识别结果
|
|
void _onSpeechResult(SpeechRecognitionResult result) {
|
|
// 如果是连续识别,发送到连续识别控制器
|
|
if (_continuousRecognitionController != null) {
|
|
if (result.finalResult) {
|
|
Logger.info('收到最终识别结果: "${result.recognizedWords}"');
|
|
_continuousRecognitionController!.add(
|
|
RecognitionEvent.finalResult(
|
|
text: result.recognizedWords,
|
|
detectedLanguage: currentLocale,
|
|
)
|
|
);
|
|
} else {
|
|
_continuousRecognitionController!.add(
|
|
RecognitionEvent(
|
|
type: RecognitionEventType.intermediateResult,
|
|
text: result.recognizedWords,
|
|
detectedLanguage: currentLocale,
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 设置当前语言
|
|
void setLocale(String localeId) {
|
|
if (_supportedLanguages.contains(localeId)) {
|
|
_currentLocale.value = localeId;
|
|
Logger.info('设置语音识别语言: $localeId');
|
|
} else {
|
|
Logger.error('不支持的语言: $localeId');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> initialize({
|
|
List<String>? supportedLanguages,
|
|
}) async {
|
|
try {
|
|
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
|
|
_supportedLanguages.clear();
|
|
_supportedLanguages.addAll(supportedLanguages);
|
|
}
|
|
|
|
// 初始化语音识别
|
|
await _initSpeech();
|
|
return _isAvailable.value;
|
|
} catch (e) {
|
|
Logger.error('语音识别初始化失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<RecognitionEvent> recognizeOnce() async {
|
|
try {
|
|
if (!_isAvailable.value) {
|
|
return RecognitionEvent.error('语音识别不可用');
|
|
}
|
|
|
|
if (_isListening) {
|
|
await stopContinuousRecognition();
|
|
}
|
|
|
|
// 创建Completer来等待最终结果
|
|
final completer = Completer<RecognitionEvent>();
|
|
|
|
// 开始监听
|
|
_asrState.value = AsrState.listening;
|
|
|
|
// 定义结果处理函数
|
|
void resultListener(SpeechRecognitionResult result) {
|
|
if (result.finalResult) {
|
|
// 只有在最终结果时才完成completer
|
|
completer.complete(RecognitionEvent.finalResult(
|
|
text: result.recognizedWords,
|
|
detectedLanguage: currentLocale,
|
|
));
|
|
}
|
|
}
|
|
|
|
// 使用统一方法启动识别,单次识别使用更短的时间参数
|
|
final success = await _startListening(
|
|
onResult: resultListener,
|
|
listenFor: const Duration(seconds: 15),
|
|
pauseFor: const Duration(seconds: 2),
|
|
);
|
|
|
|
if (!success) {
|
|
return RecognitionEvent.error('启动语音识别失败');
|
|
}
|
|
|
|
// 设置较短的超时时间
|
|
return await completer.future.timeout(
|
|
const Duration(seconds: 20), // 略长于listenFor以确保获取到结果
|
|
onTimeout: () {
|
|
_speech.stop();
|
|
return RecognitionEvent.error('语音识别超时');
|
|
},
|
|
);
|
|
} catch (e) {
|
|
Logger.error('一次性语音识别失败: $e');
|
|
return RecognitionEvent.error('$e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
|
|
try {
|
|
if (!_isAvailable.value) {
|
|
final controller = StreamController<RecognitionEvent>.broadcast();
|
|
controller.add(RecognitionEvent.error('语音识别不可用'));
|
|
controller.close();
|
|
return controller.stream;
|
|
}
|
|
|
|
// 如果已经在监听,先停止
|
|
if (_isListening) {
|
|
await stopContinuousRecognition();
|
|
}
|
|
|
|
// 创建新的流控制器
|
|
_continuousRecognitionController = StreamController<RecognitionEvent>.broadcast();
|
|
|
|
// 开始会话
|
|
_continuousRecognitionController!.add(
|
|
RecognitionEvent(type: RecognitionEventType.sessionStarted)
|
|
);
|
|
|
|
// 开始连续监听
|
|
_asrState.value = AsrState.listening;
|
|
|
|
// 使用统一方法启动识别,连续识别使用较长的参数
|
|
final success = await _startListening(
|
|
onResult: _onSpeechResult,
|
|
);
|
|
|
|
if (!success) {
|
|
// 添加错误事件,但保留流
|
|
_continuousRecognitionController!.add(
|
|
RecognitionEvent.error('启动连续语音识别失败')
|
|
);
|
|
|
|
// 返回已有的流控制器
|
|
return _continuousRecognitionController!.stream;
|
|
}
|
|
|
|
_isListening = true;
|
|
return _continuousRecognitionController!.stream;
|
|
} catch (e) {
|
|
Logger.error('启动连续语音识别失败: $e');
|
|
|
|
// 如果已经创建了控制器,使用它
|
|
if (_continuousRecognitionController != null) {
|
|
_continuousRecognitionController!.add(RecognitionEvent.error('$e'));
|
|
return _continuousRecognitionController!.stream;
|
|
} else {
|
|
// 否则创建新的控制器
|
|
final controller = StreamController<RecognitionEvent>.broadcast();
|
|
controller.add(RecognitionEvent.error('$e'));
|
|
return controller.stream;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> stopContinuousRecognition() async {
|
|
try {
|
|
// 无论是否正在监听,都尝试停止
|
|
if (_speech.isListening) {
|
|
await _speech.stop();
|
|
}
|
|
|
|
_isListening = false;
|
|
|
|
// 发送会话结束事件
|
|
_continuousRecognitionController?.add(
|
|
RecognitionEvent(type: RecognitionEventType.sessionStopped)
|
|
);
|
|
|
|
// 关闭流控制器
|
|
await _continuousRecognitionController?.close();
|
|
_continuousRecognitionController = null;
|
|
|
|
_asrState.value = AsrState.done;
|
|
return true;
|
|
} catch (e) {
|
|
Logger.error('停止连续语音识别失败: $e');
|
|
|
|
// 强制重置状态
|
|
_isListening = false;
|
|
_asrState.value = AsrState.error;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool isContinuousRecognitionActive() {
|
|
return _isListening;
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
try {
|
|
// 确保停止所有识别活动
|
|
if (_speech.isListening) {
|
|
await _speech.stop();
|
|
}
|
|
|
|
// 关闭控制器
|
|
await _continuousRecognitionController?.close();
|
|
_continuousRecognitionController = null;
|
|
|
|
_isListening = false;
|
|
_asrState.value = AsrState.notInitialized;
|
|
} catch (e) {
|
|
Logger.error('释放语音识别资源失败: $e');
|
|
}
|
|
}
|
|
}
|