Browse Source

修改名称、包名

weicu
tanlongsheng 2 years ago
parent
commit
511be0153b
  1. 4
      ios/Runner/BluetoothMediaButtonHelper.swift
  2. 2
      ios/Runner/Info.plist
  3. 285
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  4. 2
      lib/main.dart
  5. 2
      lib/modules/chat/views/chat_view.dart
  6. 6
      macos/Runner.xcodeproj/project.pbxproj
  7. 2
      macos/Runner/Configs/AppInfo.xcconfig

4
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

2
ios/Runner/Info.plist

@ -7,7 +7,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Deep Voice</string>
<string>DeepSound</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

285
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<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;
}
}
}

2
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));

2
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<ChatController> {
const ChatView({Key? key}) : super(key: key);

6
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";

2
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.

Loading…
Cancel
Save