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.
350 lines
10 KiB
350 lines
10 KiB
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
|
|
import '../asr_service.dart';
|
|
|
|
/// 火山语音识别服务实现
|
|
class VolcanoAsrService extends GetxService implements AsrService {
|
|
/// 方法通道
|
|
static const MethodChannel _channel = MethodChannel('volcano_speech/asr');
|
|
|
|
/// 事件通道
|
|
static const EventChannel _eventChannel = EventChannel('volcano_speech/asr_events');
|
|
|
|
/// 事件流控制器
|
|
final StreamController<RecognitionEvent> _eventStreamController =
|
|
StreamController<RecognitionEvent>.broadcast();
|
|
|
|
/// 事件订阅
|
|
StreamSubscription? _eventSubscription;
|
|
|
|
/// 是否初始化完成
|
|
bool _isInitialized = false;
|
|
|
|
/// 是否正在连续识别
|
|
bool _isContinuousRecognitionActive = false;
|
|
|
|
/// 当前语言
|
|
String _currentLanguage = 'zh-CN';
|
|
|
|
/// 支持的语言列表
|
|
final List<String> _supportedLanguages = ['zh-CN', 'en-US'];
|
|
|
|
@override
|
|
List<String> get supportedLanguages => _supportedLanguages;
|
|
|
|
/// 构造函数
|
|
VolcanoAsrService() {
|
|
_setupEventListener();
|
|
}
|
|
|
|
/// 初始化语音识别服务
|
|
@override
|
|
Future<bool> initialize({
|
|
List<String>? supportedLanguages,
|
|
}) async {
|
|
if (_isInitialized) return true;
|
|
|
|
try {
|
|
// 如果提供了支持的语言列表,则替换默认列表
|
|
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
|
|
_supportedLanguages.clear();
|
|
_supportedLanguages.addAll(supportedLanguages);
|
|
}
|
|
|
|
// 从.env文件读取配置
|
|
final appId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
|
|
final appToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
|
|
final resourceId = dotenv.env['VOLCANO_ASR_RESOURCE_ID'] ?? 'volc.bigasr.sauc.duration';
|
|
|
|
if (appId.isEmpty || appToken.isEmpty) {
|
|
debugPrint('火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量');
|
|
return false;
|
|
}
|
|
|
|
// 初始化火山语音ASR
|
|
final initResult = await _channel.invokeMethod<bool>('initialize', {
|
|
'appId': appId,
|
|
'appToken': appToken,
|
|
'resourceId': resourceId,
|
|
}) ?? false;
|
|
|
|
if (!initResult) {
|
|
debugPrint('火山语音ASR初始化失败');
|
|
return false;
|
|
}
|
|
|
|
// 设置默认语言、VAD参数和启用音量返回
|
|
await _configureAsr();
|
|
|
|
_isInitialized = true;
|
|
return true;
|
|
} catch (e) {
|
|
debugPrint('火山语音ASR初始化异常:$e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 配置ASR参数
|
|
Future<void> _configureAsr() async {
|
|
// 设置默认语言
|
|
await _channel.invokeMethod<bool>('setLanguage', {
|
|
'language': _currentLanguage,
|
|
});
|
|
|
|
}
|
|
|
|
/// 设置事件监听
|
|
void _setupEventListener() {
|
|
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
|
|
(dynamic event) {
|
|
if (event is! Map) return;
|
|
_handleAsrEvent(Map<String, dynamic>.from(event as Map));
|
|
},
|
|
onError: (Object error) {
|
|
debugPrint('ASR事件流错误: $error');
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: '事件流错误: $error',
|
|
));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 处理ASR事件
|
|
void _handleAsrEvent(Map<String, dynamic> event) {
|
|
final eventType = event['type'] as String?;
|
|
|
|
switch (eventType) {
|
|
case 'sessionStarted':
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.sessionStarted,
|
|
));
|
|
break;
|
|
|
|
case 'sessionStopped':
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.sessionStopped,
|
|
));
|
|
_isContinuousRecognitionActive = false;
|
|
break;
|
|
|
|
case 'recognizing':
|
|
final text = event['text'] as String?;
|
|
final language = event['language'] as String?;
|
|
|
|
if (text != null) {
|
|
debugPrint('接收到中间识别结果: $text');
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.intermediateResult,
|
|
text: text,
|
|
detectedLanguage: language ?? _currentLanguage,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case 'result':
|
|
final text = event['text'] as String?;
|
|
final language = event['language'] as String?;
|
|
|
|
if (text != null) {
|
|
debugPrint('接收到最终识别结果: $text');
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.finalResult,
|
|
text: text,
|
|
detectedLanguage: language ?? _currentLanguage,
|
|
));
|
|
}
|
|
break;
|
|
|
|
case 'error':
|
|
final errorMessage = event['message'] as String?;
|
|
|
|
debugPrint('接收到识别错误: ${errorMessage ?? "未知错误"}');
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: errorMessage ?? '未知错误',
|
|
));
|
|
_isContinuousRecognitionActive = false;
|
|
break;
|
|
|
|
case 'volumeChanged':
|
|
// 音量变化事件仅在日志中输出,不发送到事件流
|
|
final volume = event['volume'] as int?;
|
|
if (volume != null && volume > 5) { // 仅记录有意义的音量变化
|
|
debugPrint('音量变化: $volume');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// 检查初始化状态
|
|
Future<bool> _ensureInitialized() async {
|
|
if (!_isInitialized) {
|
|
return await initialize();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// 执行一次性语音识别
|
|
@override
|
|
Future<RecognitionEvent> recognizeOnce() async {
|
|
if (!await _ensureInitialized()) {
|
|
return RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: '语音识别服务未初始化',
|
|
);
|
|
}
|
|
|
|
try {
|
|
// 创建一个Completer用于等待最终结果
|
|
final completer = Completer<RecognitionEvent>();
|
|
|
|
// 创建一次性事件监听
|
|
StreamSubscription? subscription;
|
|
subscription = _eventStreamController.stream.listen((event) {
|
|
// 仅处理finalResult和error事件
|
|
if (event.type == RecognitionEventType.finalResult) {
|
|
// 识别成功,返回结果
|
|
completer.complete(event);
|
|
subscription?.cancel();
|
|
} else if (event.type == RecognitionEventType.error) {
|
|
// 识别失败,返回错误
|
|
completer.complete(event);
|
|
subscription?.cancel();
|
|
}
|
|
});
|
|
|
|
// 启动一次性识别
|
|
final started = await _channel.invokeMethod<bool>('recognizeOnce') ?? false;
|
|
|
|
if (!started) {
|
|
subscription.cancel();
|
|
return RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: '启动一次性识别失败',
|
|
);
|
|
}
|
|
|
|
// 设置超时,防止永久等待
|
|
Future.delayed(const Duration(seconds: 30), () {
|
|
if (!completer.isCompleted) {
|
|
completer.complete(RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: '识别超时',
|
|
));
|
|
subscription?.cancel();
|
|
// 停止识别
|
|
_channel.invokeMethod('stopRecognize');
|
|
}
|
|
});
|
|
|
|
// 等待结果
|
|
return await completer.future;
|
|
} catch (e) {
|
|
// 确保停止识别
|
|
try {
|
|
await _channel.invokeMethod('stopRecognize');
|
|
} catch (_) {}
|
|
|
|
return RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: e.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 开始连续语音识别
|
|
@override
|
|
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
|
|
if (!await _ensureInitialized()) {
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: '语音识别服务未初始化',
|
|
));
|
|
return _eventStreamController.stream;
|
|
}
|
|
|
|
try {
|
|
final success = await _channel.invokeMethod<bool>('startContinuousRecognition') ?? false;
|
|
|
|
if (success) {
|
|
_isContinuousRecognitionActive = true;
|
|
} else {
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: '启动连续识别失败',
|
|
));
|
|
}
|
|
} catch (e) {
|
|
_eventStreamController.add(RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: '启动连续识别异常:$e',
|
|
));
|
|
}
|
|
|
|
return _eventStreamController.stream;
|
|
}
|
|
|
|
/// 停止连续语音识别
|
|
@override
|
|
Future<bool> stopContinuousRecognition() async {
|
|
if (!_isInitialized || !_isContinuousRecognitionActive) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
final success = await _channel.invokeMethod<bool>('stopContinuousRecognition') ?? false;
|
|
|
|
if (success) {
|
|
_isContinuousRecognitionActive = false;
|
|
}
|
|
return success;
|
|
} catch (e) {
|
|
debugPrint('停止连续识别异常:$e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 推送音频数据到连续识别中
|
|
@override
|
|
Future<void> pushAudioData(Uint8List data) async {
|
|
if (!_isInitialized || !_isContinuousRecognitionActive) {
|
|
return;
|
|
}
|
|
|
|
// 注意:此功能需要在引擎初始化时设置音频输入类型为stream
|
|
// 由于当前实现默认使用麦克风输入,此方法暂时不会实际处理数据
|
|
debugPrint('警告:当前火山语音ASR使用麦克风输入,pushAudioData方法不会起作用');
|
|
}
|
|
|
|
/// 检查连续识别是否活跃
|
|
@override
|
|
bool isContinuousRecognitionActive() {
|
|
return _isContinuousRecognitionActive;
|
|
}
|
|
|
|
/// 释放资源
|
|
@override
|
|
Future<void> dispose() async {
|
|
if (_isContinuousRecognitionActive) {
|
|
await stopContinuousRecognition();
|
|
}
|
|
|
|
await _eventSubscription?.cancel();
|
|
_eventSubscription = null;
|
|
|
|
if (!_eventStreamController.isClosed) {
|
|
await _eventStreamController.close();
|
|
}
|
|
|
|
if (_isInitialized) {
|
|
await _channel.invokeMethod<bool>('release');
|
|
_isInitialized = false;
|
|
}
|
|
}
|
|
}
|