diff --git a/ios/Runner/BluetoothMediaButtonHelper.swift b/ios/Runner/BluetoothMediaButtonHelper.swift index 3d203b74d..1a8369172 100644 --- a/ios/Runner/BluetoothMediaButtonHelper.swift +++ b/ios/Runner/BluetoothMediaButtonHelper.swift @@ -230,8 +230,8 @@ class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate { // 创建音频专辑图片 let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片 var nowPlayingInfo: [String: Any] = [ - MPMediaItemPropertyTitle: "DeepVoice 语音助手", - MPMediaItemPropertyArtist: "DeepVoice", + MPMediaItemPropertyTitle: "DeepSound 语音助手", + MPMediaItemPropertyArtist: "DeepSound", MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放 MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime, MPMediaItemPropertyPlaybackDuration: totalDuration diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e55e60462..aea2275af 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -7,7 +7,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Deep Voice + DeepSound CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 7a2d6f457..bab3790ef 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -15,61 +15,61 @@ import 'package:record/record.dart'; import '../asr_service.dart'; /// Volcano语音识别服务 -/// +/// /// 负责与火山引擎实时语音识别服务的通信 class VolcanoAsrApiService implements AsrService { static final VolcanoAsrApiService to = Get.find(); - + // 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? _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 _supportedLanguages = ['zh-CN', 'en-US']; String _selectedLanguage = 'zh-CN'; - + // 结果流 StreamController? _recognitionController; - + // 事件计数器(用于调试) int _sendPacketCount = 0; int _receivePacketCount = 0; - + // AsrService接口实现 @override List get supportedLanguages => _supportedLanguages; - + @override Future initialize({ List? 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 recognizeOnce() async { try { if (!_isInitialized) { await initialize(); } - + final resultCompleter = Completer(); - + // 开始连续识别流程 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> startContinuousRecognition() async { if (!_isInitialized) { await initialize(); } - + if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } - + try { // 创建结果控制器 _recognitionController = StreamController.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.broadcast(); errorController.add(RecognitionEvent.error(e.toString())); errorController.close(); - + return errorController.stream; } } - + @override Future 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 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 _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 _connectWebSocket() async { if (_isConnecting) { @@ -294,20 +295,20 @@ class VolcanoAsrApiService implements AsrService { return _connectionCompleter!.future; } } - + _connectionCompleter = Completer(); _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?) _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 = { - '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 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 _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 _startRecording() async { // TODO: 实现正确的录音开始逻辑 @@ -497,12 +498,12 @@ class VolcanoAsrApiService implements AsrService { rethrow; } */ - + // 临时实现,防止错误 _isRecording = true; Logger.info('录音已开始(模拟)'); } - + /// 捕获并发送音频数据 Future _captureAndSendAudio() async { // TODO: 实现正确的音频捕获逻辑 @@ -545,46 +546,46 @@ class VolcanoAsrApiService implements AsrService { } */ } - + /// 发送音频数据 Future _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 _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 _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 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 data) { try { - final Map? result = data['result'] as Map?; + final Map? result = + data['result'] as Map?; 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 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 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 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 _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 _cleanupResources() async { // 停止录音 await _stopRecording(); - + // 关闭WebSocket await _closeWebSocket(); - + // 关闭结果控制器 if (_recognitionController != null && !_recognitionController!.isClosed) { await _recognitionController!.close(); _recognitionController = null; } - + _isContinuousRecognitionActive = false; _isRecognitionEndRequested = false; } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index e08049e9d..47dbe1b08 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,7 +30,7 @@ void main() async { const double width = 375.0; const double height = 812.0; - setWindowTitle('Deep Voice'); + setWindowTitle('DeepSound'); setWindowMinSize(const Size(width, height)); setWindowMaxSize(const Size(width, height)); diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index cddb49257..86d77e3a1 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -2,11 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math'; -import 'package:audio_waveforms/audio_waveforms.dart'; import '../controllers/chat_controller.dart'; import '../models/message_model.dart'; import '../../../core/theme/app_colors.dart'; -import '../../../core/widgets/message_bubble.dart'; class ChatView extends GetView { const ChatView({Key? key}) : super(key: key); diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a5036d1d0..4658cacbc 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -479,7 +479,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/deep_voice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/deep_voice"; @@ -494,7 +494,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/deep_voice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/deep_voice"; @@ -509,7 +509,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/deep_voice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/deep_voice"; diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index 99aa41de4..34460e77c 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = deep_voice // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.deepVoice +PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved.