|
|
|
@ -15,61 +15,61 @@ import 'package:record/record.dart'; |
|
|
|
import '../asr_service.dart'; |
|
|
|
|
|
|
|
/// Volcano语音识别服务 |
|
|
|
/// |
|
|
|
/// |
|
|
|
/// 负责与火山引擎实时语音识别服务的通信 |
|
|
|
class VolcanoAsrApiService implements AsrService { |
|
|
|
static final VolcanoAsrApiService to = Get.find<VolcanoAsrApiService>(); |
|
|
|
|
|
|
|
|
|
|
|
// API配置 |
|
|
|
late final String _appKey; |
|
|
|
late final String _secretKey; |
|
|
|
late final String _serviceId; |
|
|
|
late final String _hostUrl; |
|
|
|
final String _path = '/ws/v1/asr'; |
|
|
|
|
|
|
|
|
|
|
|
// 请求相关参数 |
|
|
|
final String _algorithm = 'hmac-sha1'; |
|
|
|
final Uuid _uuid = const Uuid(); |
|
|
|
|
|
|
|
|
|
|
|
// 录音配置 |
|
|
|
final int _sampleRate = 16000; |
|
|
|
final int _bitsPerSample = 16; |
|
|
|
final int _channels = 1; |
|
|
|
final String _encoding = 'pcm'; |
|
|
|
|
|
|
|
|
|
|
|
// 连接相关 |
|
|
|
WebSocketChannel? _channel; |
|
|
|
Completer<void>? _connectionCompleter; |
|
|
|
StreamSubscription? _connectionSubscription; |
|
|
|
Timer? _heartbeatTimer; |
|
|
|
|
|
|
|
|
|
|
|
// 语音录音 |
|
|
|
final Record? _recorder = null; // TODO: 实现正确的Record实例化 |
|
|
|
Timer? _audioTimer; |
|
|
|
bool _isRecording = false; |
|
|
|
|
|
|
|
|
|
|
|
// 状态控制 |
|
|
|
bool _isInitialized = false; |
|
|
|
bool _isConnecting = false; |
|
|
|
bool _isContinuousRecognitionActive = false; |
|
|
|
|
|
|
|
|
|
|
|
// 识别结果控制 |
|
|
|
bool _isRecognitionEndRequested = false; |
|
|
|
String _currentSessionId = ''; |
|
|
|
final List<String> _supportedLanguages = ['zh-CN', 'en-US']; |
|
|
|
String _selectedLanguage = 'zh-CN'; |
|
|
|
|
|
|
|
|
|
|
|
// 结果流 |
|
|
|
StreamController<RecognitionEvent>? _recognitionController; |
|
|
|
|
|
|
|
|
|
|
|
// 事件计数器(用于调试) |
|
|
|
int _sendPacketCount = 0; |
|
|
|
int _receivePacketCount = 0; |
|
|
|
|
|
|
|
|
|
|
|
// AsrService接口实现 |
|
|
|
@override |
|
|
|
List<String> get supportedLanguages => _supportedLanguages; |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
Future<bool> initialize({ |
|
|
|
List<String>? supportedLanguages, |
|
|
|
@ -79,21 +79,21 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
if (_isInitialized) { |
|
|
|
await dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_isInitialized = false; |
|
|
|
|
|
|
|
|
|
|
|
// 从环境变量加载配置 |
|
|
|
_loadConfig(); |
|
|
|
|
|
|
|
|
|
|
|
// 设置支持的语言 |
|
|
|
if (supportedLanguages != null && supportedLanguages.isNotEmpty) { |
|
|
|
_supportedLanguages.clear(); |
|
|
|
_supportedLanguages.addAll(supportedLanguages); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 初始化录音机 |
|
|
|
await _initRecorder(); |
|
|
|
|
|
|
|
|
|
|
|
_isInitialized = true; |
|
|
|
Logger.info('火山语音API已初始化'); |
|
|
|
return true; |
|
|
|
@ -102,19 +102,19 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
Future<RecognitionEvent> recognizeOnce() async { |
|
|
|
try { |
|
|
|
if (!_isInitialized) { |
|
|
|
await initialize(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
final resultCompleter = Completer<RecognitionEvent>(); |
|
|
|
|
|
|
|
|
|
|
|
// 开始连续识别流程 |
|
|
|
final stream = await startContinuousRecognition(); |
|
|
|
|
|
|
|
|
|
|
|
// 监听结果 |
|
|
|
StreamSubscription? subscription; |
|
|
|
subscription = stream.listen((event) { |
|
|
|
@ -138,7 +138,7 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
resultCompleter.complete(RecognitionEvent.error('未收到识别结果')); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 设置超时 |
|
|
|
Timer(const Duration(seconds: 30), () { |
|
|
|
if (!resultCompleter.isCompleted) { |
|
|
|
@ -147,104 +147,104 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
stopContinuousRecognition(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return resultCompleter.future; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('单次语音识别失败: $e'); |
|
|
|
return RecognitionEvent.error(e.toString()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { |
|
|
|
if (!_isInitialized) { |
|
|
|
await initialize(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (_isContinuousRecognitionActive) { |
|
|
|
await stopContinuousRecognition(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
// 创建结果控制器 |
|
|
|
_recognitionController = StreamController<RecognitionEvent>.broadcast(); |
|
|
|
|
|
|
|
|
|
|
|
// 连接WebSocket |
|
|
|
await _connectWebSocket(); |
|
|
|
|
|
|
|
|
|
|
|
// 开始录音 |
|
|
|
await _startRecording(); |
|
|
|
|
|
|
|
|
|
|
|
_isContinuousRecognitionActive = true; |
|
|
|
_isRecognitionEndRequested = false; |
|
|
|
|
|
|
|
|
|
|
|
Logger.info('连续语音识别已开始'); |
|
|
|
|
|
|
|
|
|
|
|
return _recognitionController!.stream; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('开始连续语音识别失败: $e'); |
|
|
|
await _cleanupResources(); |
|
|
|
|
|
|
|
|
|
|
|
// 返回一个包含错误的流 |
|
|
|
final errorController = StreamController<RecognitionEvent>.broadcast(); |
|
|
|
errorController.add(RecognitionEvent.error(e.toString())); |
|
|
|
errorController.close(); |
|
|
|
|
|
|
|
|
|
|
|
return errorController.stream; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
Future<bool> stopContinuousRecognition() async { |
|
|
|
if (!_isContinuousRecognitionActive) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
_isRecognitionEndRequested = true; |
|
|
|
|
|
|
|
|
|
|
|
// 先停止录音,以确保不会再发送新的音频数据 |
|
|
|
await _stopRecording(); |
|
|
|
|
|
|
|
|
|
|
|
// 发送结束指令 |
|
|
|
await _sendEndSignal(); |
|
|
|
|
|
|
|
|
|
|
|
// 等待服务器处理完所有已发送的数据 |
|
|
|
await Future.delayed(const Duration(milliseconds: 500)); |
|
|
|
|
|
|
|
|
|
|
|
// 清理资源 |
|
|
|
await _cleanupResources(); |
|
|
|
|
|
|
|
|
|
|
|
_isContinuousRecognitionActive = false; |
|
|
|
Logger.info('连续语音识别已停止'); |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('停止连续语音识别失败: $e'); |
|
|
|
|
|
|
|
|
|
|
|
// 确保资源被清理 |
|
|
|
await _cleanupResources(); |
|
|
|
_isContinuousRecognitionActive = false; |
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
bool isContinuousRecognitionActive() { |
|
|
|
return _isContinuousRecognitionActive; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
Future<void> dispose() async { |
|
|
|
try { |
|
|
|
if (_isContinuousRecognitionActive) { |
|
|
|
await stopContinuousRecognition(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 确保资源被清理 |
|
|
|
await _cleanupResources(); |
|
|
|
|
|
|
|
|
|
|
|
_isInitialized = false; |
|
|
|
Logger.info('火山语音API资源已释放'); |
|
|
|
} catch (e) { |
|
|
|
@ -253,31 +253,32 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
_isInitialized = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 从环境变量加载配置 |
|
|
|
void _loadConfig() { |
|
|
|
_appKey = dotenv.env['VOLCANO_APP_KEY'] ?? ''; |
|
|
|
_secretKey = dotenv.env['VOLCANO_SECRET_KEY'] ?? ''; |
|
|
|
_serviceId = dotenv.env['VOLCANO_SERVICE_ID'] ?? ''; |
|
|
|
_hostUrl = dotenv.env['VOLCANO_HOST_URL'] ?? 'ws-api-asr.volces.com'; |
|
|
|
|
|
|
|
|
|
|
|
if (_appKey.isEmpty || _secretKey.isEmpty || _serviceId.isEmpty) { |
|
|
|
throw Exception('未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID'); |
|
|
|
throw Exception( |
|
|
|
'未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 初始化录音机 |
|
|
|
Future<void> _initRecorder() async { |
|
|
|
try { |
|
|
|
// TODO: 正确实现录音机初始化 |
|
|
|
// _recorder = Record(); |
|
|
|
|
|
|
|
|
|
|
|
// 检查和请求权限 |
|
|
|
// final hasPermission = await _recorder!.hasPermission(); |
|
|
|
// if (!hasPermission) { |
|
|
|
// throw Exception('没有录音权限'); |
|
|
|
// } |
|
|
|
|
|
|
|
|
|
|
|
Logger.info('录音机初始化成功'); |
|
|
|
} catch (e) { |
|
|
|
// _recorder = null; |
|
|
|
@ -285,7 +286,7 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 连接WebSocket |
|
|
|
Future<void> _connectWebSocket() async { |
|
|
|
if (_isConnecting) { |
|
|
|
@ -294,20 +295,20 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
return _connectionCompleter!.future; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_connectionCompleter = Completer<void>(); |
|
|
|
_isConnecting = true; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
// 关闭现有连接 |
|
|
|
await _closeWebSocket(); |
|
|
|
|
|
|
|
|
|
|
|
// 生成新的会话ID |
|
|
|
_currentSessionId = _uuid.v4(); |
|
|
|
|
|
|
|
|
|
|
|
// 构建URL和请求头 |
|
|
|
final (url, headers) = _buildConnectionParams(); |
|
|
|
|
|
|
|
|
|
|
|
// 创建WebSocket连接 |
|
|
|
if (kIsWeb) { |
|
|
|
_channel = WebSocketChannel.connect(Uri.parse(url)); |
|
|
|
@ -317,42 +318,42 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
headers: headers, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 处理WebSocket消息 |
|
|
|
_connectionSubscription = _channel!.stream.listen( |
|
|
|
_handleWebSocketMessage, |
|
|
|
onError: _handleWebSocketError, |
|
|
|
onDone: _handleWebSocketDone, |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 开始心跳 |
|
|
|
_startHeartbeat(); |
|
|
|
|
|
|
|
|
|
|
|
// 发送开始识别指令 |
|
|
|
await _sendStartCommand(); |
|
|
|
|
|
|
|
|
|
|
|
_isConnecting = false; |
|
|
|
_connectionCompleter!.complete(); |
|
|
|
|
|
|
|
|
|
|
|
Logger.info('WebSocket连接成功'); |
|
|
|
} catch (e) { |
|
|
|
_isConnecting = false; |
|
|
|
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { |
|
|
|
_connectionCompleter!.completeError(e); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Logger.error('连接WebSocket失败: $e'); |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return _connectionCompleter!.future; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 构建连接参数 |
|
|
|
(String, Map<String, String>?) _buildConnectionParams() { |
|
|
|
final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); |
|
|
|
final nonce = _generateNonce(); |
|
|
|
|
|
|
|
|
|
|
|
// 构建表单(用于签名和查询参数) |
|
|
|
final form = { |
|
|
|
'algorithm': _algorithm, |
|
|
|
@ -362,54 +363,54 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
'session_id': _currentSessionId, |
|
|
|
'timestamp': timestamp.toString(), |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 计算签名 |
|
|
|
final signature = _calculateSignature(form); |
|
|
|
|
|
|
|
|
|
|
|
// 添加签名到表单 |
|
|
|
form['signature'] = signature; |
|
|
|
|
|
|
|
|
|
|
|
// 构建URL查询参数 |
|
|
|
final queryString = form.entries |
|
|
|
.map((e) => '${e.key}=${Uri.encodeComponent(e.value)}') |
|
|
|
.join('&'); |
|
|
|
|
|
|
|
|
|
|
|
// 构建完整URL |
|
|
|
final url = 'wss://$_hostUrl$_path?$queryString'; |
|
|
|
|
|
|
|
|
|
|
|
// 构建请求头 |
|
|
|
final headers = <String, String>{ |
|
|
|
'User-Agent': 'Deep Voice App/1.0.0', |
|
|
|
'User-Agent': 'DeepSound App/1.0.0', |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
return (url, kIsWeb ? null : headers); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 生成随机数 |
|
|
|
String _generateNonce() { |
|
|
|
final random = Random(); |
|
|
|
final randoms = List.generate(16, (_) => random.nextInt(256)); |
|
|
|
return base64Url.encode(randoms).substring(0, 16); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 计算签名 |
|
|
|
String _calculateSignature(Map<String, String> form) { |
|
|
|
// 按键排序 |
|
|
|
final sortedKeys = form.keys.toList()..sort(); |
|
|
|
|
|
|
|
|
|
|
|
// 构建待签名字符串 |
|
|
|
final signString = sortedKeys.map((key) => '$key=${form[key]}').join(''); |
|
|
|
|
|
|
|
|
|
|
|
// 使用HMAC-SHA1进行签名 |
|
|
|
final key = utf8.encode(_secretKey); |
|
|
|
final bytes = utf8.encode(signString); |
|
|
|
final hmacSha1 = Hmac(sha1, key); |
|
|
|
final digest = hmacSha1.convert(bytes); |
|
|
|
|
|
|
|
|
|
|
|
// 返回Base64编码的签名 |
|
|
|
return base64.encode(digest.bytes); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 开始心跳定时 |
|
|
|
void _startHeartbeat() { |
|
|
|
_heartbeatTimer?.cancel(); |
|
|
|
@ -417,7 +418,7 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
_sendHeartbeat(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 发送心跳包 |
|
|
|
void _sendHeartbeat() { |
|
|
|
if (_channel != null && _isContinuousRecognitionActive) { |
|
|
|
@ -426,18 +427,18 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
'type': 'heartbeat', |
|
|
|
'sequence': _sendPacketCount++, |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
_channel!.sink.add(jsonEncode(heartbeatMsg)); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('发送心跳包失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 发送开始识别指令 |
|
|
|
Future<void> _sendStartCommand() async { |
|
|
|
if (_channel == null) return; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
final startCommand = { |
|
|
|
'type': 'start', |
|
|
|
@ -454,7 +455,7 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
'max_seconds': 60, |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
_channel!.sink.add(jsonEncode(startCommand)); |
|
|
|
Logger.info('已发送开始识别指令'); |
|
|
|
} catch (e) { |
|
|
|
@ -462,7 +463,7 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 开始录音 |
|
|
|
Future<void> _startRecording() async { |
|
|
|
// TODO: 实现正确的录音开始逻辑 |
|
|
|
@ -497,12 +498,12 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
// 临时实现,防止错误 |
|
|
|
_isRecording = true; |
|
|
|
Logger.info('录音已开始(模拟)'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 捕获并发送音频数据 |
|
|
|
Future<void> _captureAndSendAudio() async { |
|
|
|
// TODO: 实现正确的音频捕获逻辑 |
|
|
|
@ -545,46 +546,46 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
} |
|
|
|
*/ |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 发送音频数据 |
|
|
|
Future<void> _sendAudioData(Uint8List data) async { |
|
|
|
if (_channel == null || data.isEmpty) return; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
final audioMessage = { |
|
|
|
'type': 'audio', |
|
|
|
'sequence': _sendPacketCount++, |
|
|
|
'audio': base64.encode(data), |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
_channel!.sink.add(jsonEncode(audioMessage)); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('发送音频数据失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 发送结束信号 |
|
|
|
Future<void> _sendEndSignal() async { |
|
|
|
if (_channel == null) return; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
final endCommand = { |
|
|
|
'type': 'end', |
|
|
|
'sequence': _sendPacketCount++, |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
_channel!.sink.add(jsonEncode(endCommand)); |
|
|
|
Logger.info('已发送结束指令'); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('发送结束指令失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 停止录音 |
|
|
|
Future<void> _stopRecording() async { |
|
|
|
_audioTimer?.cancel(); |
|
|
|
_audioTimer = null; |
|
|
|
|
|
|
|
|
|
|
|
// TODO: 实现正确的录音停止逻辑 |
|
|
|
/* |
|
|
|
if (_isRecording && _recorder != null) { |
|
|
|
@ -597,64 +598,65 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
} |
|
|
|
} |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
// 临时实现,防止错误 |
|
|
|
_isRecording = false; |
|
|
|
Logger.info('录音已停止(模拟)'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理WebSocket消息 |
|
|
|
void _handleWebSocketMessage(dynamic message) { |
|
|
|
try { |
|
|
|
_receivePacketCount++; |
|
|
|
|
|
|
|
|
|
|
|
if (message is String) { |
|
|
|
final Map<String, dynamic> data = jsonDecode(message); |
|
|
|
final String type = data['type'] as String? ?? ''; |
|
|
|
|
|
|
|
|
|
|
|
switch (type) { |
|
|
|
case 'result': |
|
|
|
_handleResultMessage(data); |
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case 'start_result': |
|
|
|
_handleStartResultMessage(data); |
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case 'end_result': |
|
|
|
_handleEndResultMessage(data); |
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case 'error': |
|
|
|
_handleErrorMessage(data); |
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case 'heartbeat_result': |
|
|
|
// 心跳响应,不需要特殊处理 |
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
default: |
|
|
|
Logger.info('收到未知类型的消息: $type'); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.error('处理WebSocket消息失败: $e'); |
|
|
|
|
|
|
|
|
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
_recognitionController!.addError('处理消息失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理结果消息 |
|
|
|
void _handleResultMessage(Map<String, dynamic> data) { |
|
|
|
try { |
|
|
|
final Map<String, dynamic>? result = data['result'] as Map<String, dynamic>?; |
|
|
|
final Map<String, dynamic>? result = |
|
|
|
data['result'] as Map<String, dynamic>?; |
|
|
|
if (result == null) return; |
|
|
|
|
|
|
|
|
|
|
|
final bool isEnd = result['is_end'] as bool? ?? false; |
|
|
|
final String text = result['text'] as String? ?? ''; |
|
|
|
|
|
|
|
|
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
if (isEnd) { |
|
|
|
// 最终结果 |
|
|
|
@ -674,11 +676,11 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
Logger.error('处理结果消息失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理开始结果消息 |
|
|
|
void _handleStartResultMessage(Map<String, dynamic> data) { |
|
|
|
final int code = data['code'] as int? ?? -1; |
|
|
|
|
|
|
|
|
|
|
|
if (code == 0) { |
|
|
|
// 开始成功 |
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
@ -689,19 +691,20 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
} else { |
|
|
|
// 开始失败 |
|
|
|
final String message = data['message'] as String? ?? '未知错误'; |
|
|
|
|
|
|
|
|
|
|
|
Logger.error('语音识别开始失败: [$code] $message'); |
|
|
|
|
|
|
|
|
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
_recognitionController!.add(RecognitionEvent.error('开始失败: [$code] $message')); |
|
|
|
_recognitionController! |
|
|
|
.add(RecognitionEvent.error('开始失败: [$code] $message')); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理结束结果消息 |
|
|
|
void _handleEndResultMessage(Map<String, dynamic> data) { |
|
|
|
final int code = data['code'] as int? ?? -1; |
|
|
|
|
|
|
|
|
|
|
|
if (code == 0) { |
|
|
|
// 结束成功 |
|
|
|
Logger.info('语音识别成功结束'); |
|
|
|
@ -710,73 +713,75 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
final String message = data['message'] as String? ?? '未知错误'; |
|
|
|
Logger.error('语音识别结束失败: [$code] $message'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 无论成功还是失败,通知会话已结束 |
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
_recognitionController!.add(RecognitionEvent( |
|
|
|
type: RecognitionEventType.sessionStopped, |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 清理资源 |
|
|
|
_cleanupResources(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理错误消息 |
|
|
|
void _handleErrorMessage(Map<String, dynamic> data) { |
|
|
|
final int code = data['code'] as int? ?? -1; |
|
|
|
final String message = data['message'] as String? ?? '未知错误'; |
|
|
|
|
|
|
|
|
|
|
|
Logger.error('收到错误消息: [$code] $message'); |
|
|
|
|
|
|
|
|
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
_recognitionController!.add(RecognitionEvent.error('[$code] $message')); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理WebSocket错误 |
|
|
|
void _handleWebSocketError(dynamic error) { |
|
|
|
Logger.error('WebSocket错误: $error'); |
|
|
|
|
|
|
|
|
|
|
|
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { |
|
|
|
_connectionCompleter!.completeError(error); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
_recognitionController!.addError(error); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 清理资源 |
|
|
|
_cleanupResources(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理WebSocket连接关闭 |
|
|
|
void _handleWebSocketDone() { |
|
|
|
Logger.info('WebSocket连接已关闭'); |
|
|
|
|
|
|
|
|
|
|
|
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { |
|
|
|
_connectionCompleter!.completeError('WebSocket连接已关闭'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 通知会话已结束 |
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed && !_isRecognitionEndRequested) { |
|
|
|
if (_recognitionController != null && |
|
|
|
!_recognitionController!.isClosed && |
|
|
|
!_isRecognitionEndRequested) { |
|
|
|
_recognitionController!.add(RecognitionEvent( |
|
|
|
type: RecognitionEventType.sessionStopped, |
|
|
|
)); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 清理资源 |
|
|
|
_cleanupResources(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 关闭WebSocket连接 |
|
|
|
Future<void> _closeWebSocket() async { |
|
|
|
_connectionSubscription?.cancel(); |
|
|
|
_connectionSubscription = null; |
|
|
|
|
|
|
|
|
|
|
|
_heartbeatTimer?.cancel(); |
|
|
|
_heartbeatTimer = null; |
|
|
|
|
|
|
|
|
|
|
|
if (_channel != null) { |
|
|
|
try { |
|
|
|
_channel!.sink.close(); |
|
|
|
@ -786,22 +791,22 @@ class VolcanoAsrApiService implements AsrService { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 清理所有资源 |
|
|
|
Future<void> _cleanupResources() async { |
|
|
|
// 停止录音 |
|
|
|
await _stopRecording(); |
|
|
|
|
|
|
|
|
|
|
|
// 关闭WebSocket |
|
|
|
await _closeWebSocket(); |
|
|
|
|
|
|
|
|
|
|
|
// 关闭结果控制器 |
|
|
|
if (_recognitionController != null && !_recognitionController!.isClosed) { |
|
|
|
await _recognitionController!.close(); |
|
|
|
_recognitionController = null; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_isContinuousRecognitionActive = false; |
|
|
|
_isRecognitionEndRequested = false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|