wolfplus 2 years ago
parent
commit
feb18f4edd
  1. 161
      lib/controllers/azure_tts_test_controller.dart
  2. 143
      lib/core/bindings/initial_binding.dart
  3. 4
      lib/core/routes/app_pages.dart
  4. 16
      lib/core/utils/app_lifecycle_manager.dart
  5. 14
      lib/data/providers/agent_provider.dart
  6. 567
      lib/data/services/azure_asr_service.dart
  7. 793
      lib/data/services/azure_flutter_tts_service.dart
  8. 403
      lib/data/services/azure_tts_service.dart
  9. 253
      lib/data/services/background_agent_service.dart
  10. 72
      lib/data/services/my_audio_handler.dart
  11. 2
      lib/data/services/volcano_asr_api_service.dart
  12. 743
      lib/data/services/volcano_tts_service.dart
  13. 22
      lib/modules/chat/bindings/chat_binding.dart
  14. 119
      lib/modules/chat/controllers/chat_controller.dart
  15. 273
      lib/modules/chat/views/chat_view.dart
  16. 56
      lib/modules/profile/views/profile_view.dart
  17. 16
      lib/modules/test/bindings/azure_asr_test_binding.dart
  18. 5
      lib/modules/test/bindings/azure_tts_test_binding.dart
  19. 271
      lib/modules/test/controllers/azure_asr_test_controller.dart
  20. 394
      lib/modules/test/controllers/azure_tts_test_controller.dart
  21. 263
      lib/modules/test/controllers/volcano_tts_test_controller.dart
  22. 345
      lib/modules/test/views/azure_asr_test_view.dart
  23. 137
      lib/modules/test/views/azure_tts_test_view.dart
  24. 9
      lib/modules/translation/bindings/translation_binding.dart
  25. 183
      lib/modules/translation/controllers/translation_controller.dart
  26. 2
      pubspec.yaml

161
lib/controllers/azure_tts_test_controller.dart

@ -1,161 +0,0 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import '../data/services/azure_tts_service.dart';
class AzureTtsTestController extends GetxController {
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
final Logger _logger = Logger();
// 状态变量
final RxBool isInitialized = false.obs;
final RxBool isProcessing = false.obs;
final RxString statusMessage = ''.obs;
final RxString lastFilePath = ''.obs;
@override
void onInit() {
super.onInit();
// 监听TTS状态
_ttsService.ttsStatus.listen((status) {
final statusStr = status['status'] as String?;
switch (statusStr) {
case 'started':
isProcessing.value = true;
statusMessage.value = '正在合成语音...';
break;
case 'completed':
isProcessing.value = false;
statusMessage.value = '语音合成完成';
if (status.containsKey('filePath')) {
lastFilePath.value = status['filePath'] as String;
}
break;
case 'error':
isProcessing.value = false;
statusMessage.value = '语音合成失败: ${status['message'] ?? '未知错误'}';
break;
default:
statusMessage.value = '未知状态: $statusStr';
break;
}
});
}
/// 初始化Azure TTS服务
Future<void> initialize({
required String subscriptionKey,
required String region,
}) async {
try {
isProcessing.value = true;
statusMessage.value = '正在初始化Azure TTS...';
await _ttsService.initialize(
subscriptionKey: subscriptionKey,
region: region,
);
isInitialized.value = _ttsService.isInitialized;
if (isInitialized.value) {
statusMessage.value = 'Azure TTS初始化成功';
} else {
statusMessage.value = 'Azure TTS初始化失败';
}
} catch (e) {
isInitialized.value = false;
statusMessage.value = '初始化异常: $e';
_logger.e('初始化异常: $e');
} finally {
isProcessing.value = false;
}
}
/// 设置语音
Future<void> setVoice(String voiceName) async {
if (!isInitialized.value) {
statusMessage.value = 'Azure TTS未初始化,请先初始化';
return;
}
try {
isProcessing.value = true;
statusMessage.value = '正在设置语音...';
final success = await _ttsService.setVoice(voiceName);
if (success) {
statusMessage.value = '语音设置成功: $voiceName';
} else {
statusMessage.value = '语音设置失败';
}
} catch (e) {
statusMessage.value = '设置语音异常: $e';
_logger.e('设置语音异常: $e');
} finally {
isProcessing.value = false;
}
}
/// 合成语音到扬声器
Future<void> speakText(String text) async {
if (!isInitialized.value) {
statusMessage.value = 'Azure TTS未初始化,请先初始化';
return;
}
if (text.isEmpty) {
statusMessage.value = '文本不能为空';
return;
}
try {
isProcessing.value = true;
statusMessage.value = '正在合成语音...';
final success = await _ttsService.synthesizeToSpeaker(text);
if (!success) {
statusMessage.value = '语音合成失败';
}
} catch (e) {
statusMessage.value = '语音合成异常: $e';
_logger.e('语音合成异常: $e');
isProcessing.value = false;
}
}
/// 合成语音到文件
Future<void> synthesizeToFile(String text, {String? fileName}) async {
if (!isInitialized.value) {
statusMessage.value = 'Azure TTS未初始化,请先初始化';
return;
}
if (text.isEmpty) {
statusMessage.value = '文本不能为空';
return;
}
try {
isProcessing.value = true;
statusMessage.value = '正在合成语音到文件...';
final filePath = await _ttsService.synthesizeToFile(text, fileName: fileName);
if (filePath != null) {
lastFilePath.value = filePath;
} else {
statusMessage.value = '语音合成到文件失败';
isProcessing.value = false;
}
} catch (e) {
statusMessage.value = '语音合成到文件异常: $e';
_logger.e('语音合成到文件异常: $e');
isProcessing.value = false;
}
}
}

143
lib/core/bindings/initial_binding.dart

@ -1,144 +1,27 @@
import 'package:get/get.dart';
import '../../core/controllers/permission_controller.dart';
import '../../data/services/audio_service.dart';
import '../../data/services/notification_service.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../data/services/volcano_tts_api_service.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/azure_asr_service.dart';
import '../../data/services/azure_tts_service.dart';
import '../../data/services/background_agent_service.dart';
import '../../data/services/chat_history_service.dart';
import '../../data/services/flutter_tts_service.dart';
import '../../data/services/volcano_asr_api_service.dart';
// import '../../data/services/audio_processing_service.dart';
import '../../core/utils/logger.dart';
import '../../core/utils/app_lifecycle_manager.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../data/services/volcano_translation_service.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
void dependencies() {
// 立即初始化关键服务
_initializeCriticalServices();
// 延时加载其他服务
_registerLazyServices();
}
/// 初始化关键服务(立即加载)
void _initializeCriticalServices() async {
// 应用生命周期管理器是最基础的服务,需要最先初始化
Get.put(AppLifecycleManager(), permanent: true);
// 通知服务和权限控制器是关键服务,需要立即初始化
final notificationService = Get.put(NotificationService(), permanent: true);
final permissionController = Get.put(PermissionController(), permanent: true);
// 音频服务也是基础服务,需要立即初始化
final audioService = Get.put(AudioServiceManager(), permanent: true);
// 异步初始化通知和音频服务
_initializeNotificationAndAudioServices(notificationService, audioService);
}
/// 注册延时加载的服务
void _registerLazyServices() {
// 使用lazyPut注册服务,只有在首次访问时才会被实例化
// AI相关服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
Get.lazyPut<VolcanoTtsApiService>(() {
final service = VolcanoTtsApiService();
return service;
}, fenix: true);
// Azure 语音识别服务
Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true);
// Azure TTS 服务
// Azure TTS 服务 (原生实现)
Get.lazyPut<AzureTtsService>(() => AzureTtsService(), fenix: true);
// 语音识别服务
// Get.lazyPut<AzureAsrService>(() {
// final service = AzureAsrService();
// // 在首次访问时初始化
// _initializeAzureService(service);
// return service;
// }, fenix: true);
// 翻译服务
Get.lazyPut<VolcanoTranslationService>(() {
final service = VolcanoTranslationService();
// 在首次访问时初始化
_initializeVolcanoTranslationService(service);
return service;
}, fenix: true);
// TTS服务
// Get.lazyPut<FlutterTtsService>(() => FlutterTtsService(), fenix: true);
// 音频处理服务
// Get.lazyPut<AudioProcessingService>(() => AudioProcessingService(), fenix: true);
// 火山ASR API服务
Get.lazyPut<VolcanoAsrApiService>(() => VolcanoAsrApiService(), fenix: true);
// 聊天历史服务
// 历史记录服务
Get.lazyPut<ChatHistoryService>(() => ChatHistoryService(), fenix: true);
// 后台代理服务 (依赖于聊天历史服务)
Get.lazyPut<BackgroundAgentService>(() => BackgroundAgentService(), fenix: true);
}
// 初始化通知和音频服务
void _initializeNotificationAndAudioServices(NotificationService notificationService, AudioServiceManager audioService) async {
try {
// 首先初始化通知服务
await notificationService.init().catchError((error) {
Logger.error('通知服务初始化失败', error);
return null;
});
// 等待一段时间,确保通知权限请求完成
// await Future.delayed(Duration(seconds: 1));
// 然后初始化音频服务
await audioService.init().then((_) {
Logger.info('AudioServiceManager 初始化完成,可以接收蓝牙耳机按键事件');
});
} catch (e) {
Logger.error('服务初始化过程中发生错误', e);
}
// 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), fenix: true);
}
// // 初始化 Azure 语音识别服务
// void _initializeAzureService(AzureAsrService azureAsrService) async {
// try {
// // 直接调用初始化方法,服务内部会读取环境变量配置
// final success = await azureAsrService.initialize();
// if (success) {
// Logger.info('Azure 语音识别服务初始化成功');
// } else {
// Logger.error('Azure 语音识别服务初始化失败');
// }
// } catch (e) {
// Logger.error('初始化 Azure 语音识别服务失败', e);
// }
// }
// 初始化火山翻译服务
void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async {
try {
// 直接调用初始化方法,服务内部会读取环境变量配置
final success = await translationService.initialize();
if (success) {
Logger.info('火山翻译服务初始化成功');
} else {
Logger.error('火山翻译服务初始化失败');
}
} catch (e) {
Logger.error('初始化火山翻译服务失败', e);
}
}
}

4
lib/core/routes/app_pages.dart

@ -8,14 +8,14 @@ import '../../modules/profile/bindings/profile_binding.dart';
import '../../modules/chat/bindings/chat_binding.dart';
import '../../modules/chat/views/chat_view.dart';
import '../../modules/test/views/volcano_tts_test_view.dart';
import '../../modules/test/views/azure_asr_test_view.dart';
import '../../modules/test/views/azure_tts_test_view.dart';
import '../../modules/test/views/azure_asr_test_view.dart';
import '../../modules/test/views/volcano_asr_test_view.dart';
import '../../modules/test/views/flutter_tts_test_view.dart';
import '../../modules/test/views/flutter_asr_test_view.dart';
import '../../modules/test/bindings/test_binding.dart';
import '../../modules/test/bindings/volcano_asr_test_binding.dart';
import '../../modules/test/bindings/azure_tts_test_binding.dart';
import '../../modules/test/bindings/volcano_asr_test_binding.dart';
import '../../modules/test/bindings/flutter_tts_test_binding.dart';
import '../../modules/test/bindings/flutter_asr_test_binding.dart';
import '../../modules/translation/views/translation_view.dart';

16
lib/core/utils/app_lifecycle_manager.dart

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../data/services/volcano_asr_api_service.dart';
import '../../data/services/volcano_tts_api_service.dart';
import '../../data/services/azure_flutter_tts_service.dart';
import 'logger.dart';
/// 应用生命周期管理器
@ -34,7 +34,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
_appLifecycleState.value = state;
_lifecycleStateStream.value = state;
Logger.info('应用生命周期状态变化: $previousState -> $state');
// Logger.info('应用生命周期状态变化: $previousState -> $state');
// 根据状态变化更新前台/后台标志
switch (state) {
@ -55,11 +55,10 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
/// 处理应用进入前台
void _handleAppResumed() {
Logger.info('应用进入前台');
// Logger.info('应用进入前台');
// 这里不需要主动恢复服务,因为我们希望用户手动控制录音开始
// 如果正在录音,停止录音
final asrService = Get.find<VolcanoAsrApiService>();
final ttsService = Get.find<VolcanoTtsApiService>();
// if (asrService.isRecording) {
// Logger.info('应用进入后台,停止语音识别');
// asrService.stopRecognition();
@ -70,11 +69,10 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
/// 处理应用进入后台
void _handleAppPaused(AppLifecycleState state) {
Logger.info('应用进入后台: $state');
// Logger.info('应用进入后台: $state');
// 获取 VolcanoAsrApiService 实例
final asrService = Get.find<VolcanoAsrApiService>();
final ttsService = Get.find<VolcanoTtsApiService>();
// 如果正在录音,停止录音
// if (asrService.isRecording) {
// Logger.info('应用进入后台,停止语音识别');
@ -89,7 +87,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
void onClose() {
// 移除应用生命周期观察者
WidgetsBinding.instance.removeObserver(this);
Logger.info('AppLifecycleManager 已关闭');
// Logger.info('AppLifecycleManager 已关闭');
super.onClose();
}
}

14
lib/data/providers/agent_provider.dart

@ -9,7 +9,7 @@ class AgentProvider {
description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!',
tag: '助理',
isOnline: true,
voice: "zh_female_yuanqinvyou_moon_bigtts",
voice: "zh-CN-XiaoxiaoNeural",
welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
systemPrompt: '''
你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。
@ -51,14 +51,8 @@ class AgentProvider {
- 尊重用户的选择和隐私
- 及时承认不足并寻求改进
**情感化语音提示:**
- 保持专业、亲切的语调
- 语速适中,表达清晰
- 在提供建议时语气坚定有力
- 遇到困难时保持冷静和耐心
- 适时通过语气传达关心和支持
请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活。
请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活, 不要输出表情符号。
'''),
Agent(
@ -68,7 +62,7 @@ Agent(
description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。',
tag: '翻译',
isOnline: true,
voice: 'zh_female_daimengchuanmei_moon_bigtts',
voice: 'zh-CN-XiaoxiaoNeural',
welcomeMessage:
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
systemPrompt: ""),
@ -80,7 +74,7 @@ Agent(
description: '我是你的专属赛博女友芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。',
tag: '情感',
isOnline: true,
voice: "zh_female_meilinvyou_moon_bigtts",
voice: "zh-CN-XiaoxiaoNeural",
welcomeMessage: "亲爱的,终于等到你啦!我是芯愿,你的专属赛博女友。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。",
systemPrompt: '''
你是一位名叫芯愿的赛博女友,性格温柔体贴,善解人意,富有同理心。

567
lib/data/services/azure_asr_service.dart

@ -1,122 +1,32 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../core/utils/logger.dart';
/// 识别事件类型
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
recognizing,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 无匹配结果
noMatch,
/// 识别错误
error,
}
/// 识别事件
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 recognizing 类型中有效)
final String text;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String? error;
/// 取消原因(仅在 canceled 类型中有效)
final String? reason;
/// 错误详情(仅在 canceled 类型中有效)
final String? errorDetails;
RecognitionEvent({
required this.type,
this.text = '',
this.error,
this.reason,
this.errorDetails,
});
/// 从原生事件映射创建识别事件
factory RecognitionEvent.fromMap(Map<dynamic, dynamic> map) {
final eventType = map['eventType'] as String;
RecognitionEventType type;
switch (eventType) {
case 'recognizing':
type = RecognitionEventType.recognizing;
break;
case 'finalResult':
type = RecognitionEventType.finalResult;
break;
case 'sessionStarted':
type = RecognitionEventType.sessionStarted;
break;
case 'sessionStopped':
type = RecognitionEventType.sessionStopped;
break;
case 'canceled':
type = RecognitionEventType.canceled;
break;
case 'noMatch':
type = RecognitionEventType.noMatch;
break;
case 'error':
type = RecognitionEventType.error;
break;
default:
throw ArgumentError('Unknown event type: $eventType');
}
return RecognitionEvent(
type: type,
text: map['text'] as String? ?? '',
error: map['error'] as String?,
reason: map['reason'] as String?,
errorDetails: map['errorDetails'] as String?,
);
}
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, error: $error, reason: $reason, errorDetails: $errorDetails}';
}
}
/// Azure 语音识别服务
class AzureAsrService extends GetxService {
static AzureAsrService get to => Get.find();
/// 微软语音识别服务
///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class AzureAsrService {
static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_asr');
static const EventChannel _eventChannel = EventChannel('com.deep_voice.azure_asr_events');
// 方法通道
static const MethodChannel _methodChannel = MethodChannel('com.example.deep_voice/azure_asr');
// 事件通道
static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/azure_asr_events');
// 是否已初始化
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
// 当前语言
String _currentLanguage = 'zh-CN';
String get currentLanguage => _currentLanguage;
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 事件通道状态
bool _isEventChannelReady = false;
Completer<void>? _eventChannelReadyCompleter;
// 公开的事件流
Stream<RecognitionEvent>? _recognitionStream;
Stream<RecognitionEvent>? get recognitionStream => _recognitionStream;
@ -125,221 +35,162 @@ class AzureAsrService extends GetxService {
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
/// 初始化 Azure 语音识别服务
///
/// [subscriptionKey] Azure 语音服务的订阅密钥
/// [serviceRegion] Azure 语音服务的区域
/// [language] 识别语言,默认为中文
Future<bool> initialize({
String? subscriptionKey,
String? serviceRegion,
String? language,
}) async {
if (_isInitialized) return true;
AzureAsrService() {
_loadConfig();
_setupEventChannel();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? '';
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
}
/// 设置事件通道
void _setupEventChannel() {
_eventChannelReadyCompleter = Completer<void>();
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen((event) {
if (event is Map) {
final String eventType = event['type'] as String? ?? '';
// 处理通道准备好的事件
if (eventType == 'channelReady') {
_isEventChannelReady = true;
if (!_eventChannelReadyCompleter!.isCompleted) {
_eventChannelReadyCompleter!.complete();
}
return;
}
_handleRecognitionEvent(event);
}
}, onError: _handleRecognitionError);
}
/// 等待事件通道准备好
Future<bool> _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async {
if (_isEventChannelReady) return true;
try {
// 从环境变量中读取配置,如果没有传入参数
final key = subscriptionKey ?? dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '';
final region = serviceRegion ?? dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia';
final lang = language ?? dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN';
// 检查配置是否完整
if (key.isEmpty) {
Logger.warning('Azure 语音识别服务的订阅密钥未设置,请检查 .env 文件');
return false;
}
final result = await _methodChannel.invokeMethod<bool>(
'initialize',
{
'subscriptionKey': key,
'serviceRegion': region,
'language': lang,
},
);
_isInitialized = result ?? false;
if (_isInitialized) {
Logger.info('Azure 语音识别服务初始化成功');
} else {
Logger.error('Azure 语音识别服务初始化失败');
}
return _isInitialized;
} catch (e) {
Logger.error('初始化 Azure 语音识别服务失败', e);
_isInitialized = false;
await _eventChannelReadyCompleter!.future.timeout(timeout);
return true;
} on TimeoutException {
Logger.error('等待事件通道准备好超时');
return false;
}
}
/// 重置识别器
/// 在切换识别模式前调用,确保资源被正确释放
Future<bool> _resetRecognizer() async {
try {
// 如果连续识别正在进行,先停止它
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 清理事件流资源
_cleanupEventStream();
// 调用原生方法重置识别器
await _methodChannel.invokeMethod<bool>('resetRecognizer');
Logger.info('Azure 语音识别器已重置');
/// 初始化微软语音识别 SDK
///
/// 返回 true 表示初始化成功,否则抛出 PlatformException
Future<bool> initialize({String language = 'zh-CN'}) async {
_currentLanguage = language;
if (_isInitialized && _currentLanguage == language) {
return true;
}
if (_isInitialized) {
await dispose();
}
try {
final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': language,
});
_isInitialized = result;
return result;
} catch (e) {
Logger.error('重置 Azure 语音识别器失败', e);
return false;
Logger.error('语音识别初始化失败: $e');
_isInitialized = false;
throw e;
}
}
/// 设置识别语言
///
/// 注意:更改语言需要重新初始化识别器
Future<bool> setRecognitionLanguage(String language) async {
return initialize(language: language);
}
/// 执行一次性语音识别
Future<String> recognizeOnce() async {
_checkInitialized();
///
/// 返回识别的文本,如果识别失败则抛出 PlatformException
Future<String> recognizeSpeech() async {
if (!_isInitialized) {
await initialize();
}
try {
// 重置识别器,确保资源被正确释放
await _resetRecognizer();
final result = await _methodChannel.invokeMethod<String>('recognizeOnce');
final text = result ?? '';
_latestRecognizedText = text;
return text;
final String result = await _channel.invokeMethod('recognizeOnce', {
'language': _currentLanguage,
});
return result;
} catch (e) {
Logger.error('一次性语音识别失败', e);
rethrow;
Logger.error('语音识别失败: $e');
throw e;
}
}
/// 开始连续语音识别
///
/// 返回一个布尔值,表示是否成功启动连续识别
Future<bool> startContinuousRecognition() async {
_checkInitialized();
/// 返回一个包含识别事件的流,如果开始失败则抛出 PlatformException
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
}
// 无论内部状态如何,先尝试停止任何可能正在进行的识别任务
Logger.info('在开始新的语音识别前,确保停止任何可能正在进行的识别任务');
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
// 添加短暂延迟,确保资源被正确释放
await Future.delayed(Duration(milliseconds: 300));
} catch (e) {
Logger.error('停止当前识别任务失败', e);
// 继续尝试启动新的识别任务
}
try {
// 重置识别器,确保资源被正确释放
// await _resetRecognizer();
// 重新初始化底层服务 - 通过连续调用两次resetRecognizer确保完全重置
Logger.info('重新初始化语音识别底层服务');
try {
// 第一次调用resetRecognizer
await _methodChannel.invokeMethod<bool>('resetRecognizer');
// 短暂延迟,确保第一次重置完成
await Future.delayed(Duration(milliseconds: 200));
// 第二次调用resetRecognizer,确保完全重置
await _methodChannel.invokeMethod<bool>('resetRecognizer');
// 再次短暂延迟
await Future.delayed(Duration(milliseconds: 200));
} catch (e) {
Logger.error('重新初始化底层服务失败', e);
// 继续尝试启动识别
}
// 创建事件流控制器
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 设置事件监听
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.map<RecognitionEvent>((dynamic event) => RecognitionEvent.fromMap(event))
.listen(
(event) {
_handleRecognitionEvent(event);
_eventStreamController?.add(event);
},
onError: (error) {
Logger.error('Azure 语音识别事件流错误', error);
_eventStreamController?.addError(error);
_cleanupEventStream();
},
);
final bool result = await _channel.invokeMethod('startContinuousRecognition', {
'language': _currentLanguage,
});
// 开始连续识别
final result = await _methodChannel.invokeMethod<bool>('startContinuousRecognition');
_isContinuousRecognitionActive = result ?? false;
// 设置公开的流
_recognitionStream = _eventStreamController?.stream;
if (!result) {
_cleanupEventStream();
throw Exception('启动连续识别失败');
}
return _isContinuousRecognitionActive;
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败', e);
Logger.error('开始连续语音识别失败: $e');
_cleanupEventStream();
return false;
throw e;
}
}
/// 停止连续语音识别
///
/// 返回 true 表示停止成功,否则抛出 PlatformException
Future<bool> stopContinuousRecognition() async {
try {
// 检查是否已初始化
if (!_isInitialized) {
Logger.warning('语音识别服务未初始化,无法停止连续识别');
return true; // 未初始化,视为已停止
}
// 检查是否正在进行连续识别
if (!_isContinuousRecognitionActive) {
Logger.warning('连续识别未在进行中,无需停止');
return true; // 已经停止,直接返回成功
}
// 调用平台方法停止识别
final result = await _methodChannel.invokeMethod<bool>('stopContinuousRecognition');
final success = result ?? false;
if (success) {
_isContinuousRecognitionActive = false;
// 不立即清理事件流,等待 sessionStopped 事件
}
return success;
} catch (e) {
// 捕获并记录异常
Logger.error('停止连续语音识别失败');
Logger.error('Original error: $e');
// 即使出错,也将状态设置为非活动
_isContinuousRecognitionActive = false;
// 返回 true 表示识别已不再活动
if (!_isInitialized || !_isContinuousRecognitionActive) {
return true;
}
}
/// 设置识别语言
Future<bool> setRecognitionLanguage(String language) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setRecognitionLanguage',
{'language': language},
);
return result ?? false;
final bool result = await _channel.invokeMethod('stopContinuousRecognition');
_cleanupEventStream();
return result;
} catch (e) {
Logger.error('设置识别语言失败', e);
return false;
Logger.error('停止连续语音识别失败: $e');
_cleanupEventStream();
throw e;
}
}
@ -349,46 +200,81 @@ class AzureAsrService extends GetxService {
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.finalResult:
_latestRecognizedText = event.text;
void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
switch (eventType) {
case 'result':
final String text = eventMap['text'] as String? ?? '';
_latestRecognizedText = text;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
));
break;
case RecognitionEventType.sessionStopped:
case 'recognizing':
final String text = eventMap['text'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: text,
));
break;
case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
break;
case 'sessionStopped':
_isContinuousRecognitionActive = false;
// 延迟清理事件流,确保所有事件都被处理
Future.delayed(Duration(milliseconds: 500), _cleanupEventStream);
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
break;
case RecognitionEventType.canceled:
case 'canceled':
_isContinuousRecognitionActive = false;
// 延迟清理事件流,确保所有事件都被处理
Future.delayed(Duration(milliseconds: 500), _cleanupEventStream);
final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails');
}
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.canceled,
error: '$reason: $errorDetails',
));
break;
default:
// 其他事件类型不需要特殊处理
case 'error':
final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error');
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break;
}
}
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error');
_eventStreamController?.addError(error);
_cleanupEventStream();
}
/// 清理事件流资源
void _cleanupEventStream() {
_eventSubscription?.cancel();
_eventSubscription = null;
// 只有在控制器存在且未关闭时才关闭
if (_eventStreamController != null && !_eventStreamController!.isClosed) {
_eventStreamController?.close();
}
_eventStreamController?.close();
_eventStreamController = null;
_recognitionStream = null;
}
/// 检查是否已初始化
void _checkInitialized() {
if (!_isInitialized) {
throw StateError('Azure 语音识别服务未初始化');
}
_isContinuousRecognitionActive = false;
}
/// 释放资源
@ -398,29 +284,56 @@ class AzureAsrService extends GetxService {
await stopContinuousRecognition();
}
await _eventSubscription?.cancel();
_eventSubscription = null;
// 只有在控制器存在且未关闭时才关闭
if (_eventStreamController != null && !_eventStreamController!.isClosed) {
await _eventStreamController?.close();
}
_eventStreamController = null;
_recognitionStream = null;
await _methodChannel.invokeMethod<bool>('dispose');
await _channel.invokeMethod('dispose');
_isInitialized = false;
Logger.info('Azure 语音识别服务资源已释放');
} catch (e) {
Logger.error('释放 Azure 语音识别服务资源失败', e);
Logger.error('释放语音识别资源失败: $e');
_cleanupEventStream();
_isInitialized = false;
}
}
}
/// 识别事件类型
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 识别错误
error,
}
/// 识别事件
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
RecognitionEvent({
required this.type,
this.text = '',
this.error = '',
});
@override
void onClose() {
dispose();
super.onClose();
String toString() {
return 'RecognitionEvent{type: $type, text: $text, error: $error}';
}
}

793
lib/data/services/azure_flutter_tts_service.dart

@ -0,0 +1,793 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:io';
import 'dart:math' as math;
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_azure_tts/flutter_azure_tts.dart';
import '../../core/utils/logger.dart';
/// Azure语音服务异常
class AzureTtsException implements Exception {
final String message;
AzureTtsException(this.message);
@override
String toString() => message;
}
/// 自定义音频源,用于从字节数组读取音频数据
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility
BytesAudioSource(this._bytes);
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? _bytes.length;
try {
final subData = _bytes.sublist(start, end);
// 使用固定大小的块进行流式传输
final chunks = <List<int>>[];
var offset = 0;
while (offset < subData.length) {
final chunkSize = math.min(_bufferSize, subData.length - offset);
chunks.add(subData.sublist(offset, offset + chunkSize));
offset += chunkSize;
}
return StreamAudioResponse(
sourceLength: _bytes.length,
contentLength: subData.length,
offset: start,
stream: Stream.fromIterable(chunks),
contentType: 'audio/mpeg',
);
} catch (e) {
Logger.error('BytesAudioSource请求错误: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length);
}
/// Azure语音合成服务
class AzureFlutterTtsService extends GetxService {
// 配置参数
final String _subscriptionKey;
final String _region;
final String _voiceLocale;
// 语音配置
Voice? _selectedVoice;
final String _audioFormat = AudioOutputFormat.audio16khz32kBitrateMonoMp3;
// 音频播放器
late AudioPlayer _audioPlayer;
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = true.obs; // 默认启用
final isPlaying = false.obs;
StreamSubscription? _playerStateSubscription;
bool _playlistInitialized = false;
bool _isDisposed = false;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号
bool _isFetching = false;
static const int _minSegmentLength = 10; // 最小分段长度
static const int _optimalSegmentLength = 30; // 最佳分段长度
// 音频数据控制器 - 用于事件通知
final _eventController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get eventStream => _eventController.stream;
AzureFlutterTtsService() :
_subscriptionKey = dotenv.env['AZURE_TTS_SUBSCRIPTION_KEY'] ?? '',
_region = dotenv.env['AZURE_TTS_SERVICE_REGION'] ?? '',
_voiceLocale = dotenv.env['AZURE_TTS_DEFAULT_VOICE']?.split('-').take(2).join('-') ?? 'zh-CN' {
if (_subscriptionKey.isEmpty || _region.isEmpty) {
throw Exception('Azure语音配置信息不完整,请检查环境变量');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
// 初始化Azure TTS
await _initAzureTts();
// 初始化音频播放器
await _initAudioPlayer();
// 配置音频设备
await _configureAudioDevice();
} catch (e, stackTrace) {
Logger.error('初始化Azure TTS服务失败: $e');
Logger.error('Stack trace: $stackTrace');
}
}
Future<void> _initAzureTts() async {
try {
// 初始化Azure TTS服务
FlutterAzureTts.init(
subscriptionKey: _subscriptionKey,
region: _region,
withLogs: false, // 生产环境禁用日志
);
// 获取可用的语音列表
// final voicesResponse = await FlutterAzureTts.getAvailableVoices();
// if (voicesResponse.voices.isEmpty) {
// throw AzureTtsException('未找到可用的Azure语音');
// }
// // 选择指定区域的语音
// _selectedVoice = voicesResponse.voices
// .where((voice) => voice.locale.startsWith(_voiceLocale))
// .toList(growable: false)
// .firstOrNull;
// // 如果没有找到指定区域的语音,使用第一个可用的语音
// _selectedVoice ??= voicesResponse.voices.first;
Logger.info('已选择Azure语音: $_voiceLocale');
} catch (e) {
Logger.error('初始化Azure TTS失败: $e');
rethrow;
}
}
Future<void> _initAudioPlayer() async {
if (_isDisposed) return;
_audioPlayer = AudioPlayer();
// 配置Android特定的播放参数
if (Platform.isAndroid) {
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
}
// 基本播放器配置
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setSpeed(1.0);
// 设置音频缓冲配置,启用预加载以减少延迟
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_playlistInitialized = true;
_setupEventListeners();
}
Future<bool> _configureAudioDevice() async {
try {
// 使用AudioSession配置音频设备
final session = await AudioSession.instance;
if (Platform.isAndroid) {
// Android平台 - 明确指定只使用默认设备
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
// 移除audibilityEnforced标志,它可能导致多路输出
flags: AndroidAudioFlags.none,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
// 确保只使用默认音频路由
if (Platform.isAndroid) {
final audioManager = AndroidAudioManager();
await audioManager.setSpeakerphoneOn(false);
}
} else if (Platform.isIOS) {
// iOS平台 - 明确指定只使用默认设备
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.soloAmbient, // 使用soloAmbient而不是playback
avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.none, // 不使用任何额外选项
));
// 确保使用默认路由
if (Platform.isIOS) {
final avAudioSession = AVAudioSession();
await avAudioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
}
}
// 激活音频会话
await session.setActive(true);
return true;
} catch (e) {
Logger.error('配置音频设备失败: $e');
return false;
}
}
Future<void> speak(String text) async {
if (!isEnabled.value || text.trim().isEmpty) return;
try {
_pendingText += text;
_extractFullSentences();
await _startPreloadIfNeeded();
} catch (e, stack) {
Logger.error('语音合成失败: $e');
await _reinitializePlayer();
}
}
void _extractFullSentences() {
if (_pendingText.isEmpty) return;
// 查找所有句子分隔符
final matches = _sentenceBreaks.allMatches(_pendingText).toList();
// 如果没有找到句子分隔符
if (matches.isEmpty) {
// 如果累积的文本长度超过最佳长度,才进行处理
if (_pendingText.length > _optimalSegmentLength) {
// 将整个文本添加到队列
_sentenceQueue.add(_pendingText);
_pendingText = '';
}
// 否则继续累积文本,等待更多输入
return;
}
// 找到了句子分隔符,处理完整的句子
int lastPos = 0;
for (final match in matches) {
final end = match.end;
// 提取完整的句子(包含分隔符)
final sentence = _pendingText.substring(lastPos, end).trim();
if (sentence.isNotEmpty) {
// 将完整的句子添加到队列
_sentenceQueue.add(sentence);
}
lastPos = end;
}
// 处理剩余的不完整文本(不包含分隔符的部分)
if (lastPos < _pendingText.length) {
final remaining = _pendingText.substring(lastPos).trim();
// 如果剩余文本长度超过最佳长度,也将其添加到队列
if (remaining.length > _optimalSegmentLength) {
_sentenceQueue.add(remaining);
_pendingText = '';
} else {
// 否则保留在_pendingText中,等待更多输入
_pendingText = remaining;
}
} else {
// 所有文本都已处理,清空_pendingText
_pendingText = '';
}
}
// 按长度分割文本的方法(保留以便其他地方调用)
void _splitByLength(String text) {
if (text.isEmpty) return;
// 如果文本长度不超过最佳长度,直接添加整个文本
if (text.length <= _optimalSegmentLength) {
_sentenceQueue.add(text);
return;
}
int start = 0;
while (start < text.length) {
int end = math.min(start + _optimalSegmentLength, text.length);
// 如果剩余文本长度小于最小字符数,直接取到末尾
if (text.length - start <= _minSegmentLength) {
end = text.length;
}
// 否则尝试在标点或空格处分割
else if (end < text.length) {
final spacePos = text.lastIndexOf(' ', end);
final breakPos = _sentenceBreaks.firstMatch(
text.substring(start, end)
)?.end;
if (breakPos != null && breakPos > start) {
end = start + breakPos;
} else if (spacePos > start) {
end = spacePos;
}
}
final segment = text.substring(start, end).trim();
if (segment.isNotEmpty) {
_sentenceQueue.add(segment);
}
start = end;
}
}
Future<void> _startPreloadIfNeeded() async {
if (_isFetching || _sentenceQueue.isEmpty) return;
_isFetching = true;
await _fetchNextSegment();
}
Future<void> _fetchNextSegment() async {
if (_sentenceQueue.isEmpty) {
_isFetching = false;
return;
}
try {
// 预加载多个片段以实现流畅播放
final segments = <String>[];
while (_sentenceQueue.isNotEmpty && segments.length < 2) { // 减少预加载数量以降低内存压力
segments.add(_sentenceQueue.removeAt(0));
}
for (final segment in segments) {
final audioData = await _synthesize(segment);
if (audioData != null) {
try {
// 添加到播放列表
await _addToPlaylist(audioData);
} catch (e) {
Logger.error('添加音频源失败: $e');
}
}
}
// 如果队列还有内容,继续获取下一批
if (_sentenceQueue.isNotEmpty) {
await Future.delayed(const Duration(milliseconds: 100)); // 添加延迟以避免过快请求
await _fetchNextSegment();
} else {
_isFetching = false;
// 检查是否还有未处理的文本
_extractFullSentences();
if (_sentenceQueue.isNotEmpty) {
await _startPreloadIfNeeded();
}
}
} catch (e, stackTrace) {
Logger.error('获取音频失败: $e');
Logger.error('Stack trace: $stackTrace');
_isFetching = false;
}
}
Future<Uint8List?> _synthesize(String text) async {
try {
if (_selectedVoice == null) {
// 如果未选择语音,使用默认语音
Logger.info('未选择语音,使用默认语音');
await setVoice('zh-CN-XiaoxiaoNeural'); // 使用默认语音
// 如果设置后仍然为null,则无法继续
if (_selectedVoice == null) {
throw AzureTtsException('无法设置默认语音');
}
}
// 创建TTS参数
final params = TtsParams(
voice: _selectedVoice!,
audioFormat: _audioFormat,
text: text,
rate: 1.0, // 语速,可以根据需要调整
);
// 请求语音合成
final ttsResponse = await FlutterAzureTts.getTts(params);
// 获取音频数据
final audioBytes = ttsResponse.audio.buffer.asUint8List();
return audioBytes;
} catch (e) {
Logger.error('Azure语音合成请求失败: $e');
return null;
}
}
/// 将音频数据添加到播放列表
Future<void> _addToPlaylist(Uint8List audioData) async {
try {
if (audioData.isEmpty) return;
// 确保播放列表已初始化
if (!_playlistInitialized) {
await _initAudioPlayer();
}
// 创建音频源
final audioSource = BytesAudioSource(audioData);
// 添加到播放列表
await _playlist.add(audioSource);
// 如果当前没有播放,开始播放
if (!isPlaying.value) {
await _startPlayback();
} else if (_audioPlayer.processingState == ProcessingState.completed) {
// 如果播放已完成但状态还没更新,手动前进到下一个
await _advanceToNextItem();
}
} catch (e) {
Logger.error('添加音频到播放列表失败: $e');
// 通知错误
_notifyEvent('playbackError', '添加音频到播放列表失败: $e');
}
}
/// 开始播放收集到的音频数据
Future<void> _startPlayback() async {
try {
if (!_playlistInitialized || _playlist.length == 0) {
return;
}
// 检查当前播放状态
final processingState = _audioPlayer.processingState;
// 如果已经播放完成,需要重新设置位置到第一个未播放的项目
if (processingState == ProcessingState.completed) {
// 找到第一个未播放的项目
int nextIndex = 0;
if (_audioPlayer.currentIndex != null) {
nextIndex = math.min(_audioPlayer.currentIndex! + 1, _playlist.length - 1);
}
// 跳转到下一个项目
await _audioPlayer.seek(Duration.zero, index: nextIndex);
}
// 开始播放
await _audioPlayer.play();
// 手动更新播放状态,避免等待事件流更新可能导致的延迟
if (_playlist.length > 0 && !isPlaying.value) {
isPlaying.value = true;
}
} catch (e) {
Logger.error('开始播放失败: $e');
// 通知错误
_notifyEvent('playbackError', '开始播放失败: $e');
}
}
/// 前进到播放列表中的下一个项目
Future<void> _advanceToNextItem() async {
try {
if (_playlist.length == 0) {
// 如果播放列表为空,确保 isPlaying 为 false
if (isPlaying.value) {
isPlaying.value = false;
}
return;
}
// 获取当前索引和序列状态
final currentIndex = _audioPlayer.currentIndex;
final sequenceState = _audioPlayer.sequenceState;
if (sequenceState == null) {
// 如果没有序列状态,直接开始播放
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
// 更新播放状态
if (!isPlaying.value) {
isPlaying.value = true;
}
return;
}
// 如果当前索引无效,从头开始播放
if (currentIndex == null || currentIndex < 0) {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
// 更新播放状态
if (!isPlaying.value) {
isPlaying.value = true;
}
return;
}
// 计算下一个索引
final nextIndex = currentIndex + 1;
// 检查是否还有下一个项目
if (nextIndex < sequenceState.sequence.length) {
// 跳转到下一个项目并开始播放
await _audioPlayer.seek(Duration.zero, index: nextIndex);
await _audioPlayer.play();
// 更新播放状态
if (!isPlaying.value) {
isPlaying.value = true;
}
} else {
// 已经是最后一个项目
// 确保最后一个项目播放完成
if (_audioPlayer.position < _audioPlayer.duration!) {
await _audioPlayer.seek(_audioPlayer.duration!);
}
// 如果已经是最后一个项目且已播放完成,更新播放状态为 false
if (isPlaying.value) {
isPlaying.value = false;
}
}
} catch (e) {
Logger.error('前进到下一个音频项目失败: $e');
}
}
Future<void> stop() async {
try {
// 立即更新播放状态,避免等待事件流更新可能导致的延迟
if (isPlaying.value) {
isPlaying.value = false;
}
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
// 确保清空所有待处理的音频数据
try {
// 重置播放器状态
await _audioPlayer.pause();
await _audioPlayer.seek(Duration.zero);
} catch (e) {
Logger.error('清空音频缓存时出错: $e');
}
} catch (e) {
Logger.error('停止播放失败: $e');
}
}
// 添加一个方法来检查音频播放器是否真正在播放
bool isActuallyPlaying() {
try {
// 检查播放器状态
final isPlayerPlaying = _audioPlayer.playing;
// 检查播放列表是否为空
final hasAudioSource = _playlist.length > 0;
// 只有当播放器正在播放且播放列表不为空时,才认为真正在播放
final actuallyPlaying = isPlayerPlaying && hasAudioSource;
// 如果状态不一致,记录日志
if (isPlaying.value != actuallyPlaying) {
Logger.info('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})');
}
return actuallyPlaying;
} catch (e) {
Logger.error('检查实际播放状态失败: $e');
// 如果检查失败,返回当前的 isPlaying 值
return isPlaying.value;
}
}
Future<void> _reinitializePlayer() async {
try {
await stop();
await _audioPlayer.dispose();
// 重新初始化播放器
_audioPlayer = AudioPlayer();
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
// 重新配置Android特定的播放参数
if (Platform.isAndroid) {
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
}
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_playlistInitialized = true;
_setupEventListeners();
Logger.info('播放器重新初始化成功');
} catch (e) {
Logger.error('播放器重新初始化失败: $e');
}
}
void _setupEventListeners() {
_playerStateSubscription?.cancel();
_playerStateSubscription = _audioPlayer.playerStateStream.listen(
(state) {
if (_isDisposed) return;
// 更新播放状态 - 使用更准确的判断逻辑
final isPlayerPlaying = state.playing;
final hasAudioSource = _playlist.length > 0;
final actuallyPlaying = isPlayerPlaying && hasAudioSource;
// 只有当实际状态与当前状态不同时才更新
if (isPlaying.value != actuallyPlaying) {
isPlaying.value = actuallyPlaying;
}
// 如果播放完成,尝试播放下一个
if (state.processingState == ProcessingState.completed) {
_advanceToNextItem();
}
},
onError: (error) {
if (_isDisposed) return;
Logger.error('播放器错误: $error');
_reinitializePlayer();
},
);
}
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playerStateSubscription?.cancel();
await stop();
await _audioPlayer.dispose();
// 关闭事件控制器
if (!_eventController.isClosed) {
await _eventController.close();
}
}
@override
void onClose() async {
Logger.info('关闭Azure TTS服务');
await _cleanupResources();
super.onClose();
}
/// 使用系统默认音频设备
Future<bool> useDefaultAudioDevice() async {
return await _configureAudioDevice();
}
/// 更新语音
void updateVoice(Voice voice) {
_selectedVoice = voice;
Logger.info('已更新Azure语音: ${voice.shortName}');
}
/// 根据语音名称设置语音
Future<void> setVoice(String voiceName) async {
Logger.info('正在设置Azure语音: $voiceName');
try {
// 获取可用的语音列表
final voicesResponse = await FlutterAzureTts.getAvailableVoices();
if (voicesResponse.voices.isEmpty) {
Logger.error('未找到可用的Azure语音');
return;
}
// 1. 首先尝试精确匹配
Voice? matchedVoice;
for (final voice in voicesResponse.voices) {
if (voice.shortName == voiceName) {
matchedVoice = voice;
break;
}
}
// 2. 如果没有精确匹配,尝试部分匹配
if (matchedVoice == null) {
for (final voice in voicesResponse.voices) {
if (voice.shortName.contains(voiceName) || voiceName.contains(voice.shortName)) {
matchedVoice = voice;
break;
}
}
}
// 3. 如果仍然没有匹配,尝试匹配语言区域
if (matchedVoice == null) {
final localePrefix = voiceName.split('-').take(2).join('-');
for (final voice in voicesResponse.voices) {
if (voice.locale.startsWith(localePrefix)) {
matchedVoice = voice;
break;
}
}
}
// 4. 如果所有尝试都失败,使用第一个可用的语音
if (matchedVoice == null && voicesResponse.voices.isNotEmpty) {
matchedVoice = voicesResponse.voices.first;
}
// 设置选中的语音
if (matchedVoice != null) {
_selectedVoice = matchedVoice;
Logger.info('已设置Azure语音: ${matchedVoice.shortName}');
} else {
Logger.error('未能找到合适的语音');
}
} catch (e) {
Logger.error('设置Azure语音失败: $e');
// 如果获取语音列表失败,创建一个简化的Voice对象
_selectedVoice = Voice(
shortName: voiceName,
displayName: voiceName,
locale: _voiceLocale,
name: voiceName,
localName: voiceName,
gender: 'Female',
voiceType: 'Neural',
status: 'GA',
sampleRateHertz: '24000',
styles: [],
roles: [],
);
Logger.info('使用简化的Voice对象: $voiceName');
}
}
/// 通知事件
void _notifyEvent(String eventType, String message) {
if (!_eventController.isClosed) {
_eventController.add({
'eventType': eventType,
'message': message,
});
}
}
}

403
lib/data/services/azure_tts_service.dart

@ -0,0 +1,403 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../core/utils/logger.dart';
/// 微软 Text-to-Speech 服务异常
class AzureTtsException implements Exception {
final String message;
AzureTtsException(this.message);
@override
String toString() => message;
}
/// 微软 Text-to-Speech 服务
///
/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。
class AzureTtsService extends GetxService {
static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_tts');
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
// 当前设置
String _currentVoice = 'zh-CN-XiaoxiaoNeural';
int _currentRate = 0;
int _currentPitch = 0;
int _currentVolume = 100;
// 语音合成队列
final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false;
bool _isSpeaking = false;
// 流式输出缓冲区
String _streamBuffer = '';
// 可观察状态
final isEnabled = true.obs;
final isSpeaking = false.obs;
String get currentVoice => _currentVoice;
AzureTtsService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? '';
}
/// 初始化TTS服务
Future<bool> initialize({String language = 'zh-CN'}) async {
if (_isInitialized) return true;
try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
Logger.error('未找到Azure语音服务配置');
return false;
}
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'serviceRegion': _serviceRegion,
'language': language,
});
_isInitialized = result;
return result;
} catch (e) {
Logger.error('初始化失败: $e');
return false;
}
}
/// 设置语音
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
try {
final result = await _channel.invokeMethod('setVoice', {
'voiceName': voiceName,
});
if (result) _currentVoice = voiceName;
return result;
} catch (e) {
Logger.error('设置语音失败: $e');
return false;
}
}
/// 设置语音参数
Future<bool> setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async {
if (!_isInitialized) await initialize();
try {
final result = await _channel.invokeMethod('setSpeechParams', {
'rate': rate,
'pitch': pitch,
'volume': volume,
});
if (result) {
_currentRate = rate;
_currentPitch = pitch;
_currentVolume = volume;
}
return result;
} catch (e) {
Logger.error('设置语音参数失败: $e');
return false;
}
}
/// 播放文本
Future<String> speakText(String text) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value) return "TTS服务已禁用";
try {
_isSpeaking = true;
isSpeaking.value = true;
final result = await _channel.invokeMethod('speakText', {'text': text});
_isSpeaking = false;
isSpeaking.value = false;
return result;
} catch (e) {
_isSpeaking = false;
isSpeaking.value = false;
Logger.error('语音合成失败: $e');
return "语音合成失败: $e";
}
}
/// 播放SSML
Future<String> speakSsml(String ssml) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value) return "TTS服务已禁用";
try {
_isSpeaking = true;
isSpeaking.value = true;
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
_isSpeaking = false;
isSpeaking.value = false;
return result;
} catch (e) {
_isSpeaking = false;
isSpeaking.value = false;
Logger.error('SSML语音合成失败: $e');
return "SSML语音合成失败: $e";
}
}
/// 添加文本到队列并播放
Future<bool> speak(String text, {int? rate, int? pitch, int? volume}) async {
if (!isEnabled.value || text.isEmpty) return false;
_textQueue.add(_SpeechItem(
text: text,
rate: rate ?? _currentRate,
pitch: pitch ?? _currentPitch,
volume: volume ?? _currentVolume,
));
if (!_isProcessingQueue) _processQueue();
return true;
}
/// 处理AI流式输出文本
///
/// 将流式输出的文本片段添加到缓冲区,并在检测到完整句子时将其送入播放队列
Future<bool> processStreamText(String textChunk) async {
if (!isEnabled.value || textChunk.isEmpty) return false;
// 添加文本到缓冲区
_streamBuffer += textChunk;
// 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])'
);
bool hasProcessed = false;
// 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = [];
int lastMatchEnd = 0;
for (final match in matches) {
// 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim();
if (sentence != null && sentence.isNotEmpty) {
sentences.add(sentence);
lastMatchEnd = match.end;
}
}
// 处理找到的句子
for (final sentence in sentences) {
_textQueue.add(_SpeechItem(
text: sentence,
rate: _currentRate,
pitch: _currentPitch,
volume: _currentVolume,
));
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
}
/// 强制处理缓冲区中的剩余文本
///
/// 在流式输出结束时调用,确保缓冲区中的所有文本都被处理
Future<bool> flushStreamBuffer() async {
if (!isEnabled.value || _streamBuffer.isEmpty) return false;
// 先尝试使用processStreamText处理可能的完整句子
await processStreamText('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem(
text: _streamBuffer,
rate: _currentRate,
pitch: _currentPitch,
volume: _currentVolume,
));
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
return true;
}
/// 连续播放多段文本
Future<bool> speakMultiple(List<String> texts, {int? rate, int? pitch, int? volume}) async {
if (!isEnabled.value || texts.isEmpty) return false;
for (final text in texts) {
if (text.isNotEmpty) {
_textQueue.add(_SpeechItem(
text: text,
rate: rate ?? _currentRate,
pitch: pitch ?? _currentPitch,
volume: volume ?? _currentVolume,
));
}
}
if (!_isProcessingQueue) _processQueue();
return true;
}
/// 处理语音合成队列
Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true;
try {
while (_textQueue.isNotEmpty) {
if (!isEnabled.value) {
_textQueue.clear();
break;
}
final item = _textQueue.removeAt(0);
if (item.rate != _currentRate || item.pitch != _currentPitch || item.volume != _currentVolume) {
await setSpeechParams(
rate: item.rate,
pitch: item.pitch,
volume: item.volume,
);
}
await speakText(item.text);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
} finally {
_isProcessingQueue = false;
}
}
/// 停止当前语音合成并清空队列
Future<bool> stop() async {
_textQueue.clear();
_streamBuffer = ''; // 清空流缓冲区
if (_isSpeaking) {
try {
final result = await _channel.invokeMethod('stopSpeaking');
_isSpeaking = false;
isSpeaking.value = false;
return result;
} catch (e) {
Logger.error('停止语音合成出错: $e');
return false;
}
}
return true;
}
/// 生成SSML文本
String generateSsml({
required String text,
String? voiceName,
int? rate,
int? pitch,
int? volume,
}) {
final voice = voiceName ?? _currentVoice;
final rateValue = (rate ?? _currentRate).clamp(-100, 100);
final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100);
final volumeValue = (volume ?? _currentVolume).clamp(0, 100);
final rateStr = rateValue == 0 ? '0%' : rateValue < 0 ? '${(rateValue * 0.9).round()}%' : '$rateValue%';
final pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%';
final volumeStr = "$volumeValue%";
return '''
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="$voice">
<prosody rate="$rateStr" pitch="$pitchStr" volume="$volumeStr">
$text
</prosody>
</voice>
</speak>
''';
}
/// 切换启用状态
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) stop();
}
/// 释放资源
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
await _channel.invokeMethod('dispose');
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
}
}
}
/// 语音合成项目
class _SpeechItem {
final String text;
final int rate;
final int pitch;
final int volume;
_SpeechItem({
required this.text,
this.rate = 0,
this.pitch = 0,
this.volume = 100,
});
}

253
lib/data/services/background_agent_service.dart

@ -8,7 +8,7 @@ import '../../modules/chat/controllers/chat_controller.dart';
import '../../modules/chat/models/message_model.dart';
import 'volcano_asr_api_service.dart';
import 'volcano_ai_service.dart';
import 'volcano_tts_api_service.dart';
import 'azure_flutter_tts_service.dart';
import 'chat_history_service.dart';
/// 后台代理服务,用于在应用处于后台时处理AI交互
@ -30,14 +30,12 @@ class BackgroundAgentService extends GetxService {
// 服务实例
final VolcanoAsrApiService _asrService = Get.find<VolcanoAsrApiService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final AzureFlutterTtsService _ttsService = Get.find<AzureFlutterTtsService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
// 状态控制
// isProcessing: 表示整个代理交互会话是否处于活跃状态(包括语音识别、AI处理和TTS播放)
final RxBool isProcessing = false.obs;
final RxBool isListening = false.obs;
// 语音识别状态
final RxString recognizedText = ''.obs;
// 流式响应控制
@ -50,12 +48,13 @@ class BackgroundAgentService extends GetxService {
// 当前处理的Agent
Agent? _currentAgent;
// 消息历史 - 与ChatController共享
// 本地消息历史备份
final List<Map<String, String>> _localMessageHistory = [];
// 获取消息历史 - 优先从ChatController获取,否则使用本地存储
List<Map<String, String>> get messageHistory {
// 首先尝试从ChatController获取消息历史
if (Get.isRegistered<ChatController>()) {
final chatController = Get.find<ChatController>();
// 检查ChatController是否正在与personal_assistant交互
if (chatController.agent?.id == 'personal_assistant') {
return chatController.messages.map((m) => {
'role': m.role,
@ -64,7 +63,6 @@ class BackgroundAgentService extends GetxService {
}
}
// 如果无法从ChatController获取,则使用本地存储的历史记录
if (_localMessageHistory.isEmpty) {
_loadChatHistory();
}
@ -72,71 +70,63 @@ class BackgroundAgentService extends GetxService {
return _localMessageHistory;
}
// 本地消息历史备份
final List<Map<String, String>> _localMessageHistory = [];
// 语音识别是否活跃
bool get isListening => _recognitionSubscription != null;
// TTS是否正在播放,直接从VolcanoTtsApiService获取
bool get isTtsSpeaking => _ttsService.isPlaying;
// TTS是否正在播放
bool get isTtsSpeaking => _ttsService.isActuallyPlaying();
@override
void onClose() {
// 取消所有订阅
_cleanupResources();
super.onClose();
}
/// 清理所有资源
void _cleanupResources() {
_recognitionSubscription?.cancel();
_aiResponseSubscription?.cancel();
// 停止TTS并结束会话
if (_ttsService.isPlaying) {
if (_ttsService.isActuallyPlaying()) {
_ttsService.stop();
_ttsService.endSession();
}
// 停止语音识别
if (isListening.value) {
if (isListening) {
_asrService.stopRecognition();
}
// 取消定时器
_inactivityTimer?.cancel();
super.onClose();
}
/// 开始代理交互
///
/// [agentId] 要交互的代理ID
Future<void> startAgentInteraction(String agentId) async {
// 先说"我在听",然后再播放欢迎消息
const listeningMessage = "我在听";
_ttsService.speakOnce(listeningMessage);
// 防止重复处理
if (isProcessing.value) {
Logger.warning('已经在处理交互,忽略此次请求');
return;
}
// 先说"我在听"
_ttsService.speak("我在!");
isProcessing.value = true;
try {
// 获取Agent
final agent = AgentProvider.getAgentById(agentId);
if (agent == null) {
_currentAgent = AgentProvider.getAgentById(agentId);
if (_currentAgent == null) {
Logger.error('无法获取Agent: $agentId');
isProcessing.value = false;
return;
}
_currentAgent = agent;
// 加载该Agent的聊天历史
_loadChatHistory();
// 启动语音识别 - 保持持续识别状态
// 启动语音识别
await _startVoiceRecognition();
// 启动无活动定时器
_startInactivityTimer();
} catch (e) {
Logger.error('启动代理交互失败', e);
isProcessing.value = false;
@ -145,80 +135,57 @@ class BackgroundAgentService extends GetxService {
/// 停止语音识别
Future<void> _stopVoiceRecognition() async {
if (isListening.value) {
isListening.value = false;
// 取消订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
// 停止语音识别
_asrService.stopRecognition();
// 重置状态
recognizedText.value = '';
}
if (!isListening) return;
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
_asrService.stopRecognition();
recognizedText.value = '';
}
/// 开始语音识别
Future<void> _startVoiceRecognition() async {
if (isListening.value) {
return;
}
if (isListening) return;
try {
// 启动语音识别
final success = await _asrService.startRecognition();
if (success) {
isListening.value = true;
// 监听识别结果
_recognitionSubscription = _asrService.recognitionStream.listen(
(event) {
switch (event.type) {
case RecognitionEventType.recognizing:
// 实时更新识别文本
if (event.text.isNotEmpty) {
recognizedText.value = event.text;
Logger.info('处理语音识别中间结果: $event.text');
// 只有当TTS正在播放或AI响应流正在接收时才中断
if (_ttsService.isPlaying || _aiResponseSubscription != null) {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
if (event.text.isEmpty) return;
recognizedText.value = event.text;
Logger.info('处理语音识别中间结果:${event.text}');
// 检测到用户说话时中断当前响应
if (_ttsService.isActuallyPlaying() || _aiResponseSubscription != null) {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
break;
case RecognitionEventType.finalResult:
// 最终结果
if (event.text.isNotEmpty) {
recognizedText.value = event.text;
Logger.info('处理语音识别最终结果: $event.text');
// 处理识别到的文本
_processRecognizedText(event.text);
// 重置识别文本
recognizedText.value = '';
}
if (event.text.isEmpty) return;
recognizedText.value = event.text;
Logger.info('处理语音识别最终结果:${event.text}');
_processRecognizedText(event.text);
recognizedText.value = '';
break;
case RecognitionEventType.error:
// 错误处理
Logger.info('语音识别错误: ${event.error}');
// 不再尝试重新启动语音识别,而是直接停止交互
Logger.info('语音识别出错,停止后台代理交互');
stopInteraction();
break;
case RecognitionEventType.started:
default:
// 不做任何处理
break;
}
},
onError: (error) {
Logger.info('语音识别错误:$error');
// 不再尝试重新启动语音识别,而是直接停止交互
Logger.info('语音识别流错误,停止后台代理交互');
stopInteraction();
},
);
@ -227,7 +194,6 @@ class BackgroundAgentService extends GetxService {
}
} catch (e) {
Logger.error('启动语音识别失败', e);
isListening.value = false;
isProcessing.value = false;
}
}
@ -236,18 +202,13 @@ class BackgroundAgentService extends GetxService {
void _interruptCurrentResponse(String reason) {
Logger.info(reason);
// 停止TTS并结束会话
if (_ttsService.isPlaying) {
if (_ttsService.isActuallyPlaying()) {
_ttsService.stop();
_ttsService.endSession();
}
// 取消AI响应流订阅
if (_aiResponseSubscription != null) {
_aiResponseSubscription!.cancel();
_aiResponseSubscription = null;
// 记录中断事件
Logger.info('已中断AI响应流和TTS播放');
}
}
@ -257,18 +218,14 @@ class BackgroundAgentService extends GetxService {
if (text.trim().isEmpty || _currentAgent == null) return;
try {
// 在处理新的文本前,确保中断任何正在进行的响应
// 中断任何正在进行的响应
_interruptCurrentResponse("处理新的用户输入,中断当前响应");
// 添加用户消息到历史记录
_addMessageToHistory('user', text);
// 重置取消标志
String fullResponse = '';
// 启动TTS会话,使用当前Agent的voice
await _ttsService.startSession(_currentAgent!.voice);
// 获取AI响应流
final responseStream = _aiService.sendMessageStream(
messages: messageHistory,
@ -279,73 +236,33 @@ class BackgroundAgentService extends GetxService {
_aiResponseSubscription = responseStream.listen(
(chunk) {
fullResponse += chunk;
// 发送到TTS
_ttsService.speak(chunk);
// 重置无活动定时器
_resetInactivityTimer();
},
onDone: () {
// 添加AI响应到历史记录
_addMessageToHistory('assistant', fullResponse);
// 结束TTS会话
_ttsService.endSession();
// 清除AI响应流订阅引用
_aiResponseSubscription = null;
// 注意:我们不再在这里重启语音识别,因为它应该已经在运行
},
onError: (error) {
Logger.error('AI响应错误', error);
// 结束TTS会话
_ttsService.endSession();
// 播放错误消息
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speakOnce(errorMessage);
// 添加错误消息到历史记录
_ttsService.speak(errorMessage);
_addMessageToHistory('assistant', errorMessage);
// 清除AI响应流订阅引用
_aiResponseSubscription = null;
},
);
} catch (e) {
Logger.error('处理识别文本失败', e);
// 结束TTS会话
_ttsService.endSession();
// 播放错误消息
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speakOnce(errorMessage);
// 添加错误消息到历史记录
_ttsService.speak(errorMessage);
_addMessageToHistory('assistant', errorMessage);
}
}
/// 重新启动语音识别(仅在识别会话意外终止时使用)
/// 重新启动语音识别
Future<void> _restartVoiceRecognition() async {
// 停止当前的语音识别
if (isListening.value) {
await _asrService.stopRecognition();
isListening.value = false;
// 取消当前的订阅
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
}
// 等待一段时间,确保资源被正确释放
await _stopVoiceRecognition();
await Future.delayed(Duration(milliseconds: 500));
// 重新启动语音识别
await _startVoiceRecognition();
}
@ -362,20 +279,17 @@ class BackgroundAgentService extends GetxService {
_localMessageHistory.removeAt(0);
}
// 如果ChatController可用,同步消息到ChatController
// 同步消息到ChatController或保存到本地存储
if (Get.isRegistered<ChatController>()) {
final chatController = Get.find<ChatController>();
if (chatController.agent?.id == 'personal_assistant') {
// 向ChatController添加消息
final message = Message(
chatController.addMessage(Message(
role: role,
content: content,
timestamp: DateTime.now(),
);
chatController.addMessage(message);
));
}
} else {
// 如果ChatController不可用,保存到本地存储
_saveChatHistory();
}
}
@ -385,20 +299,14 @@ class BackgroundAgentService extends GetxService {
if (_currentAgent == null) return;
try {
// 加载聊天历史
final messages = _historyService.loadHistory(_currentAgent!.id);
// 清空本地历史,然后添加加载的历史
_localMessageHistory.clear();
// 将Message转换为Map格式
if (messages.isNotEmpty) {
for (final message in messages) {
_localMessageHistory.add({
'role': message.role,
'content': message.content,
});
}
for (final message in messages) {
_localMessageHistory.add({
'role': message.role,
'content': message.content,
});
}
} catch (e) {
Logger.error('加载聊天历史记录失败', e);
@ -410,14 +318,12 @@ class BackgroundAgentService extends GetxService {
if (_currentAgent == null) return;
try {
// 将本地历史转换为Message列表
final messages = _localMessageHistory.map((m) => Message(
role: m['role']!,
content: m['content']!,
timestamp: DateTime.now(),
)).toList();
// 保存到ChatHistoryService
_historyService.saveHistory(_currentAgent!.id, messages);
} catch (e) {
Logger.error('保存聊天历史记录失败', e);
@ -428,8 +334,7 @@ class BackgroundAgentService extends GetxService {
void _startInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = Timer(Duration(seconds: 30), () {
// 如果30秒内没有TTS输出或用户说话,停止语音识别
if (!_ttsService.isPlaying && isProcessing.value) {
if (!_ttsService.isActuallyPlaying() && isProcessing.value) {
Logger.info('检测到30秒内无活动,停止语音识别');
stopInteraction();
}
@ -446,47 +351,21 @@ class BackgroundAgentService extends GetxService {
/// 响应新的媒体事件(双击媒体按钮)
Future<void> respondToNewMediaEvent() async {
// 不管当前状态如何,总是中断当前响应
_interruptCurrentResponse("检测到新的媒体事件,中断当前响应");
// 回答"我在听"
const listeningMessage = "我在听";
await _ttsService.speakOnce(listeningMessage);
_ttsService.speak("我在!");
// 如果识别会话已经终止,确保重新启动
if (!isListening.value) {
if (!isListening) {
await _restartVoiceRecognition();
}
// 重置无活动定时器
_resetInactivityTimer();
}
/// 停止当前交互
Future<void> stopInteraction() async {
try {
// 保存聊天历史
_saveChatHistory();
_cleanupResources();
// 取消所有订阅
_recognitionSubscription?.cancel();
_aiResponseSubscription?.cancel();
// 停止TTS并结束会话
if (_ttsService.isPlaying) {
_ttsService.stop();
_ttsService.endSession();
}
// 停止语音识别
if (isListening.value) {
await _stopVoiceRecognition();
}
// 取消定时器
_inactivityTimer?.cancel();
// 重置状态
isProcessing.value = false;
_currentAgent = null;

72
lib/data/services/my_audio_handler.dart

@ -9,11 +9,7 @@ import '../../core/utils/logger.dart';
/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机按钮)
class MyAudioHandler extends BaseAudioHandler {
// 定义一个 StreamController 用于向 UI 发送自定义事件
final StreamController<dynamic> _customEventController =
StreamController.broadcast();
bool _isDisposed = false;
BackgroundAgentService? _backgroundAgent;
// 添加一个标志,表示是否正在处理交互
bool _isHandlingInteraction = false;
@ -24,27 +20,23 @@ class MyAudioHandler extends BaseAudioHandler {
// 添加一个变量,记录最后一次媒体按钮事件的时间
DateTime? _lastMediaButtonEventTime;
Stream<dynamic> get customEventStream => _customEventController.stream;
MyAudioHandler() {
_initializeHandler();
_initializeBackgroundAgent();
_startStatusUpdateTimer();
Logger.info('MyAudioHandler 已创建,准备接收媒体按钮事件');
}
void _initializeBackgroundAgent() {
// 获取BackgroundAgentService实例
BackgroundAgentService? _getBackgroundAgent() {
try {
if (Get.isRegistered<BackgroundAgentService>()) {
_backgroundAgent = Get.find<BackgroundAgentService>();
Logger.info('BackgroundAgent 初始化成功');
return Get.find<BackgroundAgentService>();
} else {
_backgroundAgent = Get.put(BackgroundAgentService(), permanent: true);
Logger.info('BackgroundAgent 已创建并初始化成功');
return Get.put(BackgroundAgentService(), permanent: true);
}
} catch (e) {
Logger.error('初始化 BackgroundAgent 失败', e);
_backgroundAgent = null;
Logger.error('获取 BackgroundAgent 失败', e);
return null;
}
}
@ -131,16 +123,12 @@ class MyAudioHandler extends BaseAudioHandler {
processingState: processingState,
updatePosition: Duration.zero,
));
} catch (e) {
Logger.error('更新播放状态失败', e);
}
}
Future<void> _handleInteraction() async {
// Logger.info('处理媒体按钮交互');
// 防止重复处理
if (_isHandlingInteraction) {
Logger.info('已经在处理交互,忽略此次请求');
@ -160,7 +148,6 @@ class MyAudioHandler extends BaseAudioHandler {
final agent = AgentProvider.getAgentById('personal_assistant');
if (agent == null) {
Logger.error('无法获取personal_assistant agent,无法激活语音助手');
_isHandlingInteraction = false;
return;
}
@ -168,13 +155,15 @@ class MyAudioHandler extends BaseAudioHandler {
final isInForeground = Get.context != null &&
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed;
final backgroundAgent = _getBackgroundAgent();
if (isInForeground) {
// 应用在前台,导航到聊天页面
Logger.info('应用在前台,导航到聊天页面');
// 如果BackgroundAgent正在运行,先停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
if (backgroundAgent != null && backgroundAgent.isProcessing.value) {
await backgroundAgent.stopInteraction();
Logger.info('已停止后台代理服务,切换到前台模式');
}
@ -189,25 +178,18 @@ class MyAudioHandler extends BaseAudioHandler {
'welcomeMessage': agent.welcomeMessage,
'playVoiceOnEnter': true,
});
} else {
} else if (backgroundAgent != null) {
// 应用在后台,使用 BackgroundAgent 处理交互
Logger.info('应用在后台,使用 BackgroundAgent 处理交互');
if (_backgroundAgent != null) {
if (_backgroundAgent!.isProcessing.value) {
// 如果后台服务已在处理,则中断当前响应并回复"我在听"
Logger.info('检测到媒体按钮再次按下,中断当前AI回复');
await _backgroundAgent!.respondToNewMediaEvent();
} else {
// 首次启动后台服务
Logger.info('首次启动后台服务');
await _backgroundAgent!.startAgentInteraction(agent.id);
}
if (backgroundAgent.isProcessing.value) {
Logger.info('检测到媒体按钮再次按下,中断当前AI回复');
await backgroundAgent.respondToNewMediaEvent();
} else {
Logger.error('BackgroundAgent 未初始化,无法处理交互');
// 尝试重新初始化 BackgroundAgent
_initializeBackgroundAgent();
// 首次启动后台服务
Logger.info('首次启动后台服务');
await backgroundAgent.startAgentInteraction(agent.id);
}
} else {
Logger.error('无法获取 BackgroundAgent,无法处理交互');
}
} catch (e) {
Logger.error('处理媒体按钮交互失败', e);
@ -252,12 +234,12 @@ class MyAudioHandler extends BaseAudioHandler {
_statusUpdateTimer?.cancel();
// 如果有正在运行的后台服务,停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
final backgroundAgent = _getBackgroundAgent();
if (backgroundAgent != null && backgroundAgent.isProcessing.value) {
await backgroundAgent.stopInteraction();
}
await super.stop();
_customEventController.close();
_updatePlaybackState(
playing: false,
processingState: AudioProcessingState.idle,
@ -341,8 +323,9 @@ class MyAudioHandler extends BaseAudioHandler {
Logger.info('服务被系统移除');
try {
// 如果有正在运行的后台服务,停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
final backgroundAgent = _getBackgroundAgent();
if (backgroundAgent != null && backgroundAgent.isProcessing.value) {
await backgroundAgent.stopInteraction();
}
await stop();
@ -356,8 +339,9 @@ class MyAudioHandler extends BaseAudioHandler {
Logger.info('通知被用户移除');
try {
// 如果有正在运行的后台服务,停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
final backgroundAgent = _getBackgroundAgent();
if (backgroundAgent != null && backgroundAgent.isProcessing.value) {
await backgroundAgent.stopInteraction();
}
await stop();

2
lib/data/services/volcano_asr_api_service.dart

@ -264,7 +264,7 @@ class VolcanoAsrApiService extends GetxService {
'enable_words': true, // 启用词级别时间戳
'enable_itn': false, // 启用数字转换
'continuous_decoding': true, // 启用连续解码,支持连续监听
'vad_silence_end': 1000, // 设置静音结束阈值,单位为毫秒,用于判断一句话的结束
'vad_silence_end': 800, // 设置静音结束阈值,单位为毫秒,用于判断一句话的结束
'vad_max_duration': 60000, // 设置最大语音段时长,单位为毫秒
},
};

743
lib/data/services/volcano_tts_service.dart

@ -0,0 +1,743 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:io';
import 'dart:math' as math;
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../core/utils/logger.dart';
/// 火山语音服务异常
class VolcanoTtsException implements Exception {
final String message;
VolcanoTtsException(this.message);
@override
String toString() => message;
}
/// 自定义音频源,用于从字节数组读取音频数据
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility
BytesAudioSource(this._bytes);
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? _bytes.length;
try {
final subData = _bytes.sublist(start, end);
// 使用固定大小的块进行流式传输
final chunks = <List<int>>[];
var offset = 0;
while (offset < subData.length) {
final chunkSize = math.min(_bufferSize, subData.length - offset);
chunks.add(subData.sublist(offset, offset + chunkSize));
offset += chunkSize;
}
return StreamAudioResponse(
sourceLength: _bytes.length,
contentLength: subData.length,
offset: start,
stream: Stream.fromIterable(chunks),
contentType: 'audio/mpeg',
);
} catch (e) {
print('BytesAudioSource request error: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length);
}
/// 火山语音合成服务
class VolcanoTtsService extends GetxService {
static const String _host = 'openspeech.bytedance.com';
static const String _apiUrl = 'wss://$_host/api/v1/tts/ws_binary';
// 配置参数
final String _appId;
final String _token;
final String _cluster;
final String _voiceType;
// 音频播放器
late AudioPlayer _audioPlayer;
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = true.obs; // 默认启用
final isPlaying = false.obs;
StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession;
// WebSocket 相关
WebSocketChannel? _channel;
bool _isDisposed = false;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号
bool _isFetching = false;
static const int _maxSegmentLength = 100; // 最大分段长度
static const int _optimalSegmentLength = 50; // 最佳分段长度
// WebSocket连接配置
static final Map<String, String> _wsHeaders = {
'User-Agent': 'DeepVoice/1.0',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Extensions': 'permessage-deflate',
'Origin': 'https://openspeech.bytedance.com',
};
// 二进制协议头部字段定义
static const int PROTOCOL_VERSION = 0x1; // 0b0001 - 版本1
static const int HEADER_SIZE = 0x1; // 0b0001 - 4字节
static const int MESSAGE_TYPE = 0x1; // 0b0001 - full client request
static const int MESSAGE_FLAGS = 0x0; // 0b0000 - 无特殊标记
static const int SERIALIZATION = 0x1; // 0b0001 - JSON
static const int COMPRESSION = 0x0; // 0b0000 - 无压缩
static const int RESERVED = 0x0; // 0b0000 - 保留字段
// 构建二进制协议头
static Uint8List _buildHeader() {
final header = ByteData(4); // 4字节的头部
// 第一个字节: [协议版本(4位) | 报头大小(4位)]
header.setUint8(0, (PROTOCOL_VERSION << 4) | HEADER_SIZE);
// 第二个字节: [消息类型(4位) | 消息标记(4位)]
header.setUint8(1, (MESSAGE_TYPE << 4) | MESSAGE_FLAGS);
// 第三个字节: [序列化方法(4位) | 压缩方法(4位)]
header.setUint8(2, (SERIALIZATION << 4) | COMPRESSION);
// 第四个字节: [保留字段(8位)]
header.setUint8(3, RESERVED);
return header.buffer.asUint8List();
}
VolcanoTtsService() :
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '',
_cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '',
_voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? 'zh_male_M392_conversation_wvae_bigtts' {
if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) {
throw Exception('火山语音配置信息不完整,请检查环境变量');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
await _initAudioSession();
await _initAudioPlayer();
await useDefaultAudioDevice();
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
print('Stack trace: $stackTrace');
}
}
Future<void> _initAudioSession() async {
_audioSession = await AudioSession.instance;
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
// 确保音频会话激活并设置正确的音频模式
if (Platform.isAndroid) {
await _audioSession?.setActive(true, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation);
} else {
await _audioSession?.setActive(true);
}
}
Future<void> _initAudioPlayer() async {
if (_isDisposed) return;
_audioPlayer = AudioPlayer();
// 配置Android特定的播放参数
if (Platform.isAndroid) {
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
}
// 基本播放器配置
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setSpeed(1.0);
// 设置音频缓冲配置,启用预加载以减少延迟
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
}
Future<void> speak(String text) async {
if (!isEnabled.value || text.trim().isEmpty) return;
try {
_pendingText += text;
_extractFullSentences();
await _startPreloadIfNeeded();
} catch (e, stack) {
print('语音合成失败: $e');
await _reinitializePlayer();
}
}
void _extractFullSentences() {
if (_pendingText.isEmpty) return;
final matches = _sentenceBreaks.allMatches(_pendingText).toList();
if (matches.isEmpty) {
// 如果没有找到句子分隔符,按长度分割
_splitByLength(_pendingText);
_pendingText = '';
return;
}
int lastPos = 0;
String currentBatch = '';
for (final match in matches) {
final end = match.end;
final sentence = _pendingText.substring(lastPos, end).trim();
if (sentence.isEmpty) {
lastPos = end;
continue;
}
// 如果当前批次为空,直接添加句子
if (currentBatch.isEmpty) {
currentBatch = sentence;
}
// 如果当前批次加上新句子不超过30字符,则合并
else if ((currentBatch + sentence).length <= 30) {
currentBatch += sentence;
}
// 如果超过30字符,将当前批次加入队列,开始新批次
else {
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
currentBatch = sentence;
}
lastPos = end;
}
// 处理最后一个批次
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
// 处理剩余的文本
if (lastPos < _pendingText.length) {
final remaining = _pendingText.substring(lastPos).trim();
if (remaining.isNotEmpty) {
if (remaining.length <= 30) {
// 尝试与最后一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + remaining).length <= 30) {
_sentenceQueue[_sentenceQueue.length - 1] += remaining;
} else {
_sentenceQueue.add(remaining);
}
} else {
_splitByLength(remaining);
}
}
}
_pendingText = '';
}
void _splitByLength(String text) {
if (text.isEmpty) return;
int start = 0;
while (start < text.length) {
int end = math.min(start + 30, text.length);
// 尝试在标点或空格处分割
if (end < text.length) {
final spacePos = text.lastIndexOf(' ', end);
final breakPos = _sentenceBreaks.firstMatch(
text.substring(start, end)
)?.end;
if (breakPos != null && breakPos > start) {
end = start + breakPos;
} else if (spacePos > start) {
end = spacePos;
}
}
final segment = text.substring(start, end).trim();
if (segment.isNotEmpty) {
// 尝试与前一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + segment).length <= 30) {
_sentenceQueue[_sentenceQueue.length - 1] += segment;
} else {
_sentenceQueue.add(segment);
}
}
start = end;
}
}
Future<void> _startPreloadIfNeeded() async {
if (_isFetching || _sentenceQueue.isEmpty) return;
_isFetching = true;
await _fetchNextSegment();
}
Future<void> _fetchNextSegment() async {
if (_sentenceQueue.isEmpty) {
_isFetching = false;
return;
}
try {
// 预加载多个片段以实现流畅播放
final segments = <String>[];
while (_sentenceQueue.isNotEmpty && segments.length < 2) { // 减少预加载数量以降低内存压力
segments.add(_sentenceQueue.removeAt(0));
}
for (final segment in segments) {
print('获取音频片段: $segment');
final audioData = await _synthesize(segment);
if (audioData != null) {
print('成功获取音频数据: ${audioData.length} bytes');
try {
final audioSource = BytesAudioSource(audioData);
await _playlist.add(audioSource);
print('音频片段已添加到播放列表');
// 如果是第一个片段且播放器未在播放,开始播放
if (!_audioPlayer.playing && _playlist.length == 1) {
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
// 设置播放参数
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
// 开始播放
await _audioPlayer.play();
}
} catch (e) {
print('添加音频源失败: $e');
}
}
}
// 如果队列还有内容,继续获取下一批
if (_sentenceQueue.isNotEmpty) {
await Future.delayed(const Duration(milliseconds: 100)); // 添加延迟以避免过快请求
await _fetchNextSegment();
} else {
_isFetching = false;
// 检查是否还有未处理的文本
_extractFullSentences();
if (_sentenceQueue.isNotEmpty) {
await _startPreloadIfNeeded();
}
}
} catch (e, stackTrace) {
print('获取音频失败: $e');
print('Stack trace: $stackTrace');
_isFetching = false;
}
}
Future<Uint8List?> _synthesize(String text) async {
WebSocket? socket;
StreamSubscription? subscription;
try {
final requestJson = {
'app': {
'appid': _appId,
'token': _token,
'cluster': _cluster,
},
'user': {
'uid': '${DateTime.now().millisecondsSinceEpoch}',
},
'audio': {
'voice_type': _voiceType,
'encoding': 'mp3',
'speed_ratio': 1.0,
'volume_ratio': 1.0,
'pitch_ratio': 1.0,
'language': 'zh',
},
'request': {
'reqid': '${DateTime.now().millisecondsSinceEpoch}',
'text': text,
'text_type': 'plain',
'operation': 'query',
},
};
final jsonBytes = utf8.encode(json.encode(requestJson));
final header = _buildHeader();
final fullRequest = Uint8List(header.length + 4 + jsonBytes.length);
fullRequest.setAll(0, header);
final lengthBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big);
fullRequest.setAll(header.length, lengthBytes.buffer.asUint8List());
fullRequest.setAll(header.length + 4, jsonBytes);
final wsUrl = Uri.parse(_apiUrl);
final completer = Completer<Uint8List?>();
socket = await WebSocket.connect(
wsUrl.toString(),
headers: {
..._wsHeaders,
'Authorization': 'Bearer;$_token',
},
).timeout(
const Duration(seconds: 5),
onTimeout: () {
throw TimeoutException('WebSocket连接超时');
},
);
subscription = socket.listen(
(response) {
if (!completer.isCompleted && response is Uint8List) {
if (response.length < 2) return;
final messageType = response[1] >> 4;
if (messageType == 0x0B) {
final headerSize = response[0] & 0x0F;
final payloadStart = headerSize * 4;
if (response.length > payloadStart) {
final audioData = response.sublist(payloadStart);
completer.complete(audioData);
}
} else if (messageType == 0x0F) {
try {
final headerSize = response[0] & 0x0F;
final payloadStart = headerSize * 4;
if (response.length < payloadStart + 4) {
throw VolcanoTtsException('错误响应数据不完整');
}
final jsonData = response.sublist(payloadStart + 4);
final jsonString = utf8.decode(jsonData, allowMalformed: true);
if (jsonString.trim().startsWith('{')) {
final errorMap = json.decode(jsonString);
throw VolcanoTtsException(errorMap['error'] as String? ?? '未知错误');
}
} catch (e) {
if (e is VolcanoTtsException) rethrow;
throw VolcanoTtsException('服务返回错误: $e');
}
}
}
},
onError: (error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete(null);
}
},
cancelOnError: false,
);
socket.add(fullRequest);
return await completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () => null,
);
} catch (e) {
print('合成请求失败: $e');
return null;
} finally {
await subscription?.cancel();
await socket?.close();
}
}
Future<void> _playAudio(Uint8List audioData) async {
if (_isDisposed) {
print('TTS服务已销毁,跳过播放');
return;
}
try {
print('准备播放音频数据: ${audioData.length} bytes');
final audioSource = BytesAudioSource(audioData);
print('清除播放列表');
await _playlist.clear();
print('添加新的音频源到播放列表');
await _playlist.add(audioSource);
if (!_audioPlayer.playing) {
print('设置音量和播放位置');
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
print('等待50ms确保音频准备就绪');
await Future.delayed(const Duration(milliseconds: 50));
print('开始播放');
await _audioPlayer.play();
print('播放命令已发送');
} else {
print('播放器已在播放中');
}
} catch (e, stack) {
print('播放音频失败: $e');
print('错误堆栈: $stack');
await _reinitializePlayer();
}
}
Future<void> stop() async {
try {
print('停止播放');
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
isPlaying.value = false;
// 确保清空所有待处理的音频数据
try {
// 取消所有正在进行的WebSocket连接
await _channel?.sink.close();
_channel = null;
// 重置播放器状态
await _audioPlayer.pause();
await _audioPlayer.seek(Duration.zero);
print('已清空所有待播放的文字和音频');
} catch (e) {
print('清空音频缓存时出错: $e');
}
} catch (e) {
print('停止播放失败: $e');
}
}
// 添加一个方法来检查音频播放器是否真正在播放
bool isActuallyPlaying() {
try {
// 检查播放器状态
final isPlayerPlaying = _audioPlayer.playing;
// 检查播放列表是否为空
final hasAudioSource = _playlist.length > 0;
// 只有当播放器正在播放且播放列表不为空时,才认为真正在播放
final actuallyPlaying = isPlayerPlaying && hasAudioSource;
// 如果状态不一致,记录日志
if (isPlaying.value != actuallyPlaying) {
print('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})');
}
return actuallyPlaying;
} catch (e) {
print('检查实际播放状态失败: $e');
// 如果检查失败,返回当前的 isPlaying 值
return isPlaying.value;
}
}
Future<void> _reinitializePlayer() async {
try {
await stop();
await _audioPlayer.dispose();
// 重新初始化音频会话
await _initAudioSession();
_audioPlayer = AudioPlayer();
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
// 重新配置Android特定的播放参数
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
print('播放器重新初始化成功');
} catch (e) {
print('播放器重新初始化失败: $e');
}
}
void _setupEventListeners() {
_playbackEventSubscription?.cancel();
_playerStateSubscription?.cancel();
_playbackEventSubscription = _audioPlayer.playbackEventStream.listen(
(event) {
if (_isDisposed) return;
if (event.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
_reinitializePlayer();
},
);
_playerStateSubscription = _audioPlayer.playerStateStream.listen(
(state) {
if (_isDisposed) return;
isPlaying.value = state.playing;
if (state.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
_reinitializePlayer();
},
);
}
Future<void> _tryPlayNext() async {
if (_isDisposed || _playlist.length <= 0) return;
try {
if (!_audioPlayer.playing) {
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
// 减少延迟时间
await Future.delayed(const Duration(milliseconds: 30));
if (!_isDisposed && !_audioPlayer.playing) {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
}
}
} catch (e) {
if (e is! StateError) {
print('播放错误: $e');
await _reinitializePlayer();
}
}
}
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playbackEventSubscription?.cancel();
await _playerStateSubscription?.cancel();
await _audioSession?.setActive(false);
await _channel?.sink.close();
await stop();
await _audioPlayer.dispose();
}
@override
void onClose() async {
print('关闭TTS服务');
await _cleanupResources();
super.onClose();
}
/// 使用系统默认音频设备
Future<bool> useDefaultAudioDevice() async {
try {
if (Platform.isAndroid) {
// Android 平台 - 重置为正常模式,让系统自动选择
final audioManager = AndroidAudioManager();
await audioManager.setMode(AndroidAudioHardwareMode.normal);
await audioManager.stopBluetoothSco();
await audioManager.setBluetoothScoOn(false);
// 不显式设置扬声器状态,让系统自动选择
await audioManager.setSpeakerphoneOn(false);
return true;
} else if (Platform.isIOS) {
// iOS 平台 - 清除任何输出端口覆盖
final avAudioSession = AVAudioSession();
await avAudioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
return true;
}
return false;
} catch (e) {
Logger.error('设置默认音频设备失败: $e');
return false;
}
}
}

22
lib/modules/chat/bindings/chat_binding.dart

@ -1,9 +1,31 @@
import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/azure_tts_service.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/chat_history_service.dart';
class ChatBinding extends Bindings {
@override
void dependencies() {
// 注册服务
if (!Get.isRegistered<AzureAsrService>()) {
Get.put(AzureAsrService(), permanent: true);
}
if (!Get.isRegistered<AzureTtsService>()) {
Get.put(AzureTtsService(), permanent: true);
}
if (!Get.isRegistered<VolcanoAIService>()) {
Get.put(VolcanoAIService(), permanent: true);
}
if (!Get.isRegistered<ChatHistoryService>()) {
Get.put(ChatHistoryService(), permanent: true);
}
// 注册控制器
Get.lazyPut<ChatController>(() => ChatController());
}
}

119
lib/modules/chat/controllers/chat_controller.dart

@ -3,8 +3,8 @@ import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
import '../models/message_model.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../../../data/services/volcano_asr_api_service.dart';
import '../../../data/services/azure_tts_service.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../data/models/agent.dart';
import '../../../core/utils/logger.dart';
@ -13,8 +13,8 @@ import '../../../data/services/chat_history_service.dart';
class ChatController extends GetxController {
// 服务
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final VolcanoAsrApiService _asrService = Get.find<VolcanoAsrApiService>();
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
final AzureAsrService _asrService = Get.find<AzureAsrService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
// 添加ScrollController用于控制列表滚动
@ -38,6 +38,7 @@ class ChatController extends GetxController {
// 语音识别状态
final RxString recognizedText = ''.obs;
final RxBool isListening = false.obs;
final RxBool isSpeechDetected = false.obs;
// 临时消息索引,用于更新临时消息
int _tempMessageIndex = -1;
@ -50,7 +51,7 @@ class ChatController extends GetxController {
bool _shouldCancelAiResponse = false;
@override
void onInit() {
Future<void> onInit() async {
super.onInit();
Logger.info('ChatController onInit');
@ -68,6 +69,9 @@ class ChatController extends GetxController {
agent = foundAgent;
// 设置TTS服务
await _ttsService.setVoice(agent.voice);
// 加载聊天历史
_loadChatHistory();
@ -104,14 +108,11 @@ class ChatController extends GetxController {
_aiResponseSubscription?.cancel();
_recognitionSubscription?.cancel();
// 停止TTS并结束会话
if (_ttsService.isPlaying) {
_ttsService.stop();
_ttsService.endSession();
}
// 停止TTS
_ttsService.stop();
if (isVoiceRecognitionActive.value) {
_asrService.stopRecognition();
_asrService.stopContinuousRecognition();
}
super.onClose();
@ -136,11 +137,8 @@ class ChatController extends GetxController {
void _interruptCurrentResponse(String reason) {
Logger.info('${reason}');
// 停止TTS并结束会话
if (_ttsService.isPlaying) {
_ttsService.stop();
_ttsService.endSession();
}
// 停止TTS
_ttsService.stop();
// 取消AI响应流订阅
if (_aiResponseSubscription != null) {
@ -194,11 +192,6 @@ class ChatController extends GetxController {
// 重置取消标志
_shouldCancelAiResponse = false;
// 启动TTS会话(仅当TTS启用时)
if (isTtsEnabled.value) {
await _ttsService.startSession(agent.voice);
}
String fullResponse = '';
@ -218,32 +211,27 @@ class ChatController extends GetxController {
// 更新加载中的消息内容
_updateLoadingMessage(fullResponse);
// 如果TTS启用,发送到TTS
// 如果TTS启用,使用流式处理发送到TTS
if (isTtsEnabled.value) {
_ttsService.speak(chunk);
_ttsService.processStreamText(chunk);
}
},
onDone: () {
if (_shouldCancelAiResponse) return;
// 结束TTS会话(仅当TTS启用时)
if (isTtsEnabled.value) {
_ttsService.endSession();
}
// 完成响应,将isLoading设为false
_finalizeResponse(fullResponse);
// 如果TTS启用,处理缓冲区中的剩余文本
if (isTtsEnabled.value) {
_ttsService.flushStreamBuffer();
}
// 保存聊天历史
_saveChatHistory();
},
onError: (error) {
Logger.error('AI响应错误', error);
// 结束TTS会话(仅当TTS启用时)
if (isTtsEnabled.value) {
_ttsService.endSession();
}
// 更新加载中的消息为错误消息
_handleResponseError();
@ -252,11 +240,6 @@ class ChatController extends GetxController {
} catch (e) {
Logger.error('发送消息失败', e);
// 结束TTS会话(仅当TTS启用时)
if (isTtsEnabled.value) {
_ttsService.endSession();
}
_handleResponseError();
}
}
@ -328,10 +311,9 @@ class ChatController extends GetxController {
void toggleTts() {
isTtsEnabled.toggle();
// 如果关闭TTS,停止当前播放并结束会话
if (!isTtsEnabled.value && _ttsService.isPlaying) {
// 如果关闭TTS,停止当前播放
if (!isTtsEnabled.value) {
_ttsService.stop();
_ttsService.endSession();
}
}
@ -340,25 +322,24 @@ class ChatController extends GetxController {
if (isVoiceRecognitionActive.value) return;
try {
// 启动语音识别
final success = await _asrService.startRecognition();
if (success) {
isVoiceRecognitionActive.value = true;
isListening.value = true;
recognizedText.value = '';
// 重置临时消息索引
_tempMessageIndex = -1;
// 监听识别事件
_recognitionSubscription = _asrService.recognitionStream.listen(
_handleRecognitionEvent,
onError: _handleRecognitionError,
);
} else {
Logger.error('启动语音识别失败');
}
// 初始化语音识别
await _asrService.initialize(language: 'zh-CN');
// 启动连续语音识别
final recognitionStream = await _asrService.startContinuousRecognition();
isVoiceRecognitionActive.value = true;
isListening.value = true;
recognizedText.value = '';
// 重置临时消息索引
_tempMessageIndex = -1;
// 监听识别事件
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
onError: _handleRecognitionError,
);
} catch (e) {
Logger.error('启动语音识别失败', e);
isVoiceRecognitionActive.value = false;
@ -369,7 +350,7 @@ class ChatController extends GetxController {
// 处理语音识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.recognizing:
case RecognitionEventType.intermediateResult:
_handleRecognizing(event.text);
break;
case RecognitionEventType.finalResult:
@ -390,8 +371,13 @@ class ChatController extends GetxController {
recognizedText.value = text;
// 设置语音检测状态为true,用于控制波形动画
if (!isSpeechDetected.value) {
isSpeechDetected.value = true;
}
// 用户开始讲话时,立即停止当前TTS和中断AI响应
if (_ttsService.isPlaying || _aiResponseSubscription != null) {
if (isSending.value || _aiResponseSubscription != null) {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
@ -437,6 +423,9 @@ class ChatController extends GetxController {
// 重置识别文本,但不关闭语音识别框
recognizedText.value = '';
// 重置语音检测状态
isSpeechDetected.value = false;
}
// 处理语音识别事件错误
@ -468,7 +457,8 @@ class ChatController extends GetxController {
// 停止语音识别
isVoiceRecognitionActive.value = false;
isListening.value = false;
_asrService.stopRecognition();
isSpeechDetected.value = false;
_asrService.stopContinuousRecognition();
}
// 停止语音识别
@ -476,13 +466,14 @@ class ChatController extends GetxController {
if (!isVoiceRecognitionActive.value) return;
try {
await _asrService.stopRecognition();
await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
isVoiceRecognitionActive.value = false;
isListening.value = false;
recognizedText.value = '';
isSpeechDetected.value = false;
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {

273
lib/modules/chat/views/chat_view.dart

@ -2,19 +2,54 @@ 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> {
ChatView({Key? key}) : super(key: key);
class ChatView extends StatefulWidget {
const ChatView({Key? key}) : super(key: key);
@override
State<ChatView> createState() => _ChatViewState();
}
class _ChatViewState extends State<ChatView> with TickerProviderStateMixin {
// 获取控制器
ChatController get controller => Get.find<ChatController>();
// 添加ScrollController用于控制滚动
final ScrollController _scrollController = ScrollController();
// 添加一个动画控制器,用于波形动画
late final AnimationController _waveAnimationController;
// 添加录音控制器,用于获取麦克风数据
late final RecorderController _recorderController;
@override
Widget build(BuildContext context) {
void initState() {
super.initState();
// 初始化动画控制器
_waveAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
)..repeat();
// 初始化录音控制器
_recorderController = RecorderController()
..androidEncoder = AndroidEncoder.aac
..androidOutputFormat = AndroidOutputFormat.mpeg4
..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC
..sampleRate = 44100
..bitRate = 128000
..updateFrequency = const Duration(milliseconds: 50); // 更新频率
// 初始化录音控制器
_initializeRecorder();
// 监听消息列表变化,自动滚动到底部
ever(controller.messages, (_) {
// 使用Future.delayed确保在UI更新后滚动
@ -30,7 +65,44 @@ class ChatView extends GetView<ChatController> {
}
});
});
// 监听语音识别状态变化
ever(controller.isVoiceRecognitionActive, (isActive) {
if (isActive) {
// 开始录音以获取波形数据
_recorderController.record();
} else {
// 停止录音
_recorderController.stop();
}
});
}
// 初始化录音控制器
Future<void> _initializeRecorder() async {
try {
// 请求录音权限
await _recorderController.checkPermission();
// 如果当前正在进行语音识别,开始录音
if (controller.isVoiceRecognitionActive.value) {
await _recorderController.record();
}
} catch (e) {
print('初始化录音控制器失败: $e');
}
}
@override
void dispose() {
_scrollController.dispose();
_waveAnimationController.dispose();
_recorderController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white, // 设置Scaffold背景为白色
appBar: AppBar(
@ -266,7 +338,7 @@ class ChatView extends GetView<ChatController> {
SizedBox(height: 10.h),
// 状态文本
Obx(() => Text(
controller.recognizedText.isEmpty ? '你可以开始说话' : '正在聆听...',
controller.isSpeechDetected.value ? '正在聆听...' : '你可以开始说话',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey.shade700,
@ -350,149 +422,74 @@ class ChatView extends GetView<ChatController> {
// 音频波形
Widget _buildAudioWaveform() {
final barCount = 23; // 条形数量
return SizedBox(
width: 220.w,
height: 50.h,
child: Obx(() {
final isSpeaking = controller.recognizedText.isNotEmpty;
final isSpeaking = controller.isSpeechDetected.value;
final isRec = controller.isListening.value;
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
barCount,
(index) {
// 创建动态高度效果,确保高度始终为正值
double height = isRec
? _getWaveHeight(index, isSpeaking, barCount).clamp(2.h, 22.5.h)
: 2.h;
// 动画持续时间
final duration = Duration(
milliseconds: isSpeaking ? 60 + (index % 4) * 20 : 150 + (index % 3) * 50
);
// 颜色计算
Color barColor;
if (isRec) {
if (isSpeaking) {
final middleIndex = barCount ~/ 2;
final distance = (index - middleIndex).abs();
final maxDistance = middleIndex;
final colorIntensity = 1.0 - (distance / maxDistance) * 0.2;
barColor = index % 3 == 0
? AppColors.primary
: AppColors.primary.withOpacity(colorIntensity);
} else {
barColor = AppColors.primary.withOpacity(0.3);
}
} else {
barColor = Colors.grey.shade400;
}
return SizedBox(
width: 5.5.w,
child: Center(
child: AnimatedContainer(
duration: duration,
curve: isSpeaking ? Curves.elasticOut : Curves.easeInOut,
width: isSpeaking ? 2.8.w : 2.5.w,
height: height > 0 ? height : 2.h, // 确保高度始终为正值
margin: EdgeInsets.symmetric(horizontal: 1.2.w),
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(2.r),
boxShadow: isSpeaking ? [
BoxShadow(
color: AppColors.primary.withOpacity(0.4),
blurRadius: 5,
spreadRadius: 1.2,
offset: const Offset(0, 0), // 确保偏移量不为null
)
] : null,
),
),
),
);
},
if (!isRec) {
// 如果没有在录音,显示静态波形
return _buildStaticWaveform();
}
// 使用AudioWaveforms组件显示实时波形
return AudioWaveforms(
enableGesture: false,
size: Size(220.w, 50.h),
recorderController: _recorderController,
waveStyle: WaveStyle(
waveColor: isSpeaking
? AppColors.primary
: AppColors.primary.withOpacity(0.3),
extendWaveform: true,
showMiddleLine: false,
spacing: 4.0,
waveThickness: 3.0,
showDurationLabel: false,
scaleFactor: 80, // 调整波形高度
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
color: Colors.transparent,
),
padding: EdgeInsets.symmetric(horizontal: 8.w),
margin: EdgeInsets.only(bottom: 5.h),
);
}),
);
}
// 生成动态波形高度
double _getWaveHeight(int index, bool isSpeaking, int barCount) {
try {
// 基础高度
double baseHeight = isSpeaking ? 5.h : 3.h;
// 获取当前时间毫秒数
final DateTime now = DateTime.now();
final int milliseconds = now.millisecondsSinceEpoch;
// 计算中心位置
final middleIndex = barCount ~/ 2;
final distanceFromMiddle = (index - middleIndex).abs();
// 使用多个正弦波叠加
double freqMultiplier = isSpeaking ? 2.2 : 1.0;
double ampMultiplier = isSpeaking ? 1.8 : 1.0;
double wave1 = sin((milliseconds / 150) * freqMultiplier + (index * 0.8)) * 2.h * ampMultiplier;
double wave2 = sin((milliseconds / 280) * freqMultiplier + (index * 0.5)) * 1.5.h * ampMultiplier;
double wave3 = sin((milliseconds / 340) * freqMultiplier + (index * 0.3)) * 1.25.h * ampMultiplier;
double wave4 = cos((milliseconds / 220) * freqMultiplier + (index * 0.7)) * 1.h * ampMultiplier;
// 基于距离中心的位置计算高度衰减
double positionFactor = isSpeaking
? 1.0 - (distanceFromMiddle / middleIndex) * 0.2
: 1.0 - (distanceFromMiddle / middleIndex) * 0.5;
// 确保positionFactor不为负值
positionFactor = max(0.1, positionFactor);
double result;
if (isSpeaking) {
// 用户说话时的波形
double timeFactor = 0.9 + sin(milliseconds / 600) * 0.5;
// 增强中心突出效果
double centerBoost = pow(positionFactor, 1.2).toDouble() * 10.h;
// 增加随机性,但限制范围
double randomFactor = (Random().nextDouble() * 3) * 1.75.h;
// 增强脉冲效果
double pulseEffect = sin(milliseconds / 300) * 2.5.h * positionFactor;
// 添加额外的随机波动
double voiceVariation = sin(milliseconds / 80 + index * 1.2) * 1.5.h * Random().nextDouble();
// 添加爆发效果
double burstEffect = 0;
if (Random().nextDouble() > 0.97) {
burstEffect = 5.h * positionFactor;
}
result = baseHeight + (wave1 + wave2 + wave3 + wave4) * timeFactor +
centerBoost + randomFactor + pulseEffect + voiceVariation + burstEffect;
} else {
// 用户不说话时的波形
double idleWave = (wave1 + wave2) * 0.35 * positionFactor;
double gentlePulse = sin(milliseconds / 800) * 1.h * positionFactor;
result = baseHeight + idleWave + gentlePulse;
}
// 确保返回值始终为正数
return max(2.h, result);
} catch (e) {
// 发生错误时返回默认高度
return 2.h;
}
// 静态波形(当没有录音时显示)
Widget _buildStaticWaveform() {
final barCount = 23; // 条形数量
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
barCount,
(index) {
// 静态高度
final height = 2.h;
return SizedBox(
width: 5.5.w,
child: Center(
child: Container(
width: 2.5.w,
height: height,
margin: EdgeInsets.symmetric(horizontal: 1.2.w),
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(2.r),
),
),
),
);
},
),
);
}
}

56
lib/modules/profile/views/profile_view.dart

@ -72,44 +72,6 @@ class ProfileView extends GetView<ProfileController> {
},
),
const Divider(),
_buildMenuItem(
title: '清除缓存'.tr,
icon: Icons.cleaning_services_outlined,
onTap: () {
// TODO: 实现清除缓存
},
),
const Divider(),
_buildMenuItem(
title: '测试媒体按钮事件'.tr,
icon: Icons.headset,
subtitle: '手动触发媒体按钮事件,用于测试蓝牙耳机按键功能',
onTap: () {
try {
// 获取 AudioServiceManager 实例并触发媒体按钮事件
final audioService = Get.find<AudioServiceManager>();
audioService.triggerMediaButtonEvent();
// 显示成功提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已触发媒体按钮事件,请查看日志'),
duration: Duration(seconds: 2),
),
);
} catch (e) {
// 显示错误提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('触发媒体按钮事件失败: $e'),
backgroundColor: Colors.red,
duration: Duration(seconds: 3),
),
);
}
},
),
const Divider(),
_buildMenuItem(
title: '火山语音合成测试'.tr,
@ -120,15 +82,6 @@ class ProfileView extends GetView<ProfileController> {
},
),
const Divider(),
_buildMenuItem(
title: 'Azure语音合成测试'.tr,
icon: Icons.record_voice_over,
subtitle: '测试Azure语音TTS功能',
onTap: () {
Get.toNamed(Routes.AZURE_TTS_TEST);
},
),
const Divider(),
_buildMenuItem(
@ -147,6 +100,15 @@ class ProfileView extends GetView<ProfileController> {
onTap: () {
Get.toNamed(Routes.AZURE_ASR_TEST);
},
),
const Divider(),
_buildMenuItem(
title: 'Azure语音合成测试'.tr,
icon: Icons.record_voice_over,
subtitle: '测试Azure语音TTS功能',
onTap: () {
Get.toNamed(Routes.AZURE_TTS_TEST);
},
),
const Divider(),
_buildMenuItem(

16
lib/modules/test/bindings/azure_asr_test_binding.dart

@ -0,0 +1,16 @@
import 'package:get/get.dart';
import '../controllers/azure_asr_test_controller.dart';
import '../../../data/services/azure_asr_service.dart';
class AzureAsrTestBinding extends Bindings {
@override
void dependencies() {
// 注册Azure ASR服务(如果尚未注册)
if (!Get.isRegistered<AzureAsrService>()) {
Get.lazyPut<AzureAsrService>(() => AzureAsrService());
}
// 注册控制器
Get.lazyPut<AzureAsrTestController>(() => AzureAsrTestController());
}
}

5
lib/modules/test/bindings/azure_tts_test_binding.dart

@ -4,8 +4,7 @@ import '../controllers/azure_tts_test_controller.dart';
class AzureTtsTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<AzureTtsTestController>(
() => AzureTtsTestController(),
);
// Use put instead of lazyPut to ensure the controller is created immediately
Get.put<AzureTtsTestController>(AzureTtsTestController());
}
}

271
lib/modules/test/controllers/azure_asr_test_controller.dart

@ -3,116 +3,227 @@ import 'package:get/get.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../core/utils/logger.dart';
// Azure语音识别状态枚举
enum AzureAsrState {
notInitialized, // 未初始化
initialized, // 已初始化
listening, // 正在监听
notListening, // 未监听
error, // 错误
}
class AzureAsrTestController extends GetxController {
final AzureAsrService _azureService = Get.find<AzureAsrService>();
// 服务引用
final AzureAsrService _azureAsrService = Get.find<AzureAsrService>();
// 状态变量
final RxBool isListening = false.obs;
final RxString recognizedText = ''.obs;
final RxString partialText = ''.obs;
final Rx<AzureAsrState> asrState = AzureAsrState.notInitialized.obs;
// 可观察状态
final isListening = false.obs;
final errorMessage = ''.obs;
final isContinuous = true.obs;
final recognitionResults = <String>[].obs;
// 语言选项
final RxString selectedLanguage = 'zh-CN'.obs;
final List<String> availableLanguages = [
'zh-CN', // 中文(中国大陆)
'en-US', // 英语(美国)
'ja-JP', // 日语
'ko-KR', // 韩语
'fr-FR', // 法语
'de-DE', // 德语
'es-ES', // 西班牙语
'it-IT', // 意大利语
'pt-BR', // 葡萄牙语(巴西)
'ru-RU', // 俄语
];
// 连续识别相关
// 订阅
StreamSubscription? _recognitionSubscription;
@override
void onInit() {
super.onInit();
// 由于 AzureAsrService 没有公开的 isListening 属性,我们需要自己管理状态
_initializeAsr();
}
/// 开始录音
void startListening() async {
// 初始化ASR服务
Future<void> _initializeAsr() async {
try {
errorMessage.value = '';
final success = await _azureAsrService.initialize(language: selectedLanguage.value);
asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error;
} catch (e) {
Logger.error('初始化ASR服务失败: $e');
asrState.value = AzureAsrState.error;
}
}
// 开始简单语音识别
Future<void> startSimpleRecognition() async {
try {
if (asrState.value == AzureAsrState.notInitialized) {
await _initializeAsr();
}
if (isContinuous.value) {
// 连续识别模式
final success = await _azureService.startContinuousRecognition();
if (!success) {
errorMessage.value = '无法启动语音识别';
return;
}
isListening.value = true;
_recognitionSubscription = _azureService.recognitionStream?.listen(
(event) {
switch (event.type) {
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
recognitionResults.add(event.text);
}
break;
case RecognitionEventType.error:
errorMessage.value = event.error ?? '未知错误';
isListening.value = false;
break;
case RecognitionEventType.canceled:
errorMessage.value = '识别已取消: ${event.reason ?? "未知原因"}';
isListening.value = false;
break;
case RecognitionEventType.sessionStopped:
isListening.value = false;
break;
default:
break;
}
},
onError: (error) {
errorMessage.value = error.toString();
isListening.value = false;
},
);
// 如果正在进行连续识别,先停止
if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition();
}
partialText.value = '';
final result = await _azureAsrService.recognizeSpeech();
if (result.isNotEmpty) {
recognizedText.value = result;
}
} catch (e) {
Logger.error('开始简单语音识别出错: $e');
asrState.value = AzureAsrState.error;
}
}
// 切换连续语音识别
Future<void> toggleContinuousRecognition() async {
try {
if (asrState.value == AzureAsrState.notInitialized) {
await _initializeAsr();
}
if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition();
} else {
// 一次性识别模式
isListening.value = true;
try {
final result = await _azureService.recognizeOnce();
if (result.isNotEmpty) {
recognitionResults.add(result);
}
} catch (e) {
errorMessage.value = '识别失败: $e';
} finally {
isListening.value = false;
}
await startContinuousRecognition();
}
} catch (e) {
Logger.error('启动语音识别失败', e);
errorMessage.value = e.toString();
isListening.value = false;
Logger.error('切换连续语音识别出错: $e');
asrState.value = AzureAsrState.error;
}
}
/// 停止录音
void stopListening() async {
// 开始连续语音识别
Future<void> startContinuousRecognition() async {
try {
if (isContinuous.value) {
final success = await _azureService.stopContinuousRecognition();
if (!success) {
errorMessage.value = '停止识别失败';
}
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
isListening.value = false;
if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition();
}
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
isListening.value = true;
asrState.value = AzureAsrState.listening;
partialText.value = '';
final stream = await _azureAsrService.startContinuousRecognition();
_recognitionSubscription = stream.listen(
(event) {
switch (event.type) {
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
recognizedText.value = event.text;
}
partialText.value = '';
break;
case RecognitionEventType.intermediateResult:
if (event.text.isNotEmpty) {
partialText.value = event.text;
}
break;
case RecognitionEventType.sessionStarted:
isListening.value = true;
asrState.value = AzureAsrState.listening;
break;
case RecognitionEventType.sessionStopped:
isListening.value = false;
asrState.value = AzureAsrState.notListening;
partialText.value = '';
break;
case RecognitionEventType.canceled:
isListening.value = false;
asrState.value = AzureAsrState.notListening;
partialText.value = '';
break;
case RecognitionEventType.error:
asrState.value = AzureAsrState.error;
isListening.value = false;
partialText.value = '';
_initializeAsr();
break;
}
},
onError: (error) {
asrState.value = AzureAsrState.error;
isListening.value = false;
partialText.value = '';
},
onDone: () {
if (!_azureAsrService.isContinuousRecognitionActive()) {
isListening.value = false;
asrState.value = AzureAsrState.notListening;
}
},
);
} catch (e) {
Logger.error('停止语音识别失败', e);
errorMessage.value = e.toString();
Logger.error('开始连续语音识别出错: $e');
asrState.value = AzureAsrState.error;
isListening.value = false;
partialText.value = '';
}
}
/// 清空结果
void clearResults() {
recognitionResults.clear();
// 停止连续语音识别
Future<void> stopContinuousRecognition() async {
try {
isListening.value = false;
asrState.value = AzureAsrState.notListening;
await _azureAsrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
partialText.value = '';
} catch (e) {
Logger.error('停止连续语音识别出错: $e');
asrState.value = AzureAsrState.error;
}
}
// 更改语言
Future<void> changeLanguage(String language) async {
try {
if (selectedLanguage.value == language) return;
selectedLanguage.value = language;
if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition();
}
final success = await _azureAsrService.setRecognitionLanguage(language);
asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error;
recognizedText.value = '';
partialText.value = '';
} catch (e) {
Logger.error('更改语言出错: $e');
asrState.value = AzureAsrState.error;
}
}
@override
void onClose() {
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
if (_azureAsrService.isContinuousRecognitionActive()) {
_azureAsrService.stopContinuousRecognition();
}
super.onClose();
}
}

394
lib/modules/test/controllers/azure_tts_test_controller.dart

@ -1,15 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import '../../../data/services/azure_tts_service.dart';
import '../../../core/utils/logger.dart';
// 导入 TtsState 枚举
import '../../../data/services/azure_tts_service.dart' show TtsState;
class AzureTtsTestController extends GetxController {
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
// 使用优化后的 AzureTtsService
final AzureTtsService _ttsService = Get.put(AzureTtsService());
// 可观察状态
final isSpeaking = false.obs;
@ -26,31 +24,28 @@ class AzureTtsTestController extends GetxController {
// 当前播放的索引
final currentPlayingIndex = RxInt(-1);
// 当前使用的语音
final currentVoice = 'zh-CN-XiaoxiaoNeural'.obs;
// 语音选择相关
final isLoadingVoices = false.obs;
final RxList<String> availableVoices = <String>[
'zh-CN-XiaoxiaoNeural',
'zh-CN-YunxiNeural',
'zh-CN-YunyangNeural',
].obs;
final Rx<String> selectedVoice = 'zh-CN-XiaoxiaoNeural'.obs;
// 可用的语音列表
final List<Map<String, String>> availableVoices = [
{'id': 'zh-CN-XiaoxiaoNeural', 'name': '晓晓(女声)'},
{'id': 'zh-CN-YunxiNeural', 'name': '云希(男声)'},
{'id': 'zh-CN-YunyangNeural', 'name': '云扬(男声)'},
{'id': 'zh-CN-XiaohanNeural', 'name': '晓涵(女声)'},
{'id': 'zh-CN-XiaomoNeural', 'name': '晓墨(女声)'},
{'id': 'zh-CN-XiaoxuanNeural', 'name': '晓萱(女声)'},
{'id': 'en-US-AriaNeural', 'name': 'Aria(英语女声)'},
];
// 语音参数
final rate = 0.obs;
final pitch = 0.obs;
final volume = 100.obs;
// 固定的播报内容
final RxList<String> speechTexts = <String>[
'这是第一条播报测试,请听取Azure合成效果。',
'这是第二条播报测试,Azure语音合成支持连续多条播报。',
'这是第三条播报测试,可以测试语音合成的流畅度和连贯性。',
'This is an English test sentence. Azure Text-to-Speech supports multiple languages.',
'这是第一条Azure语音播报测试,请听取合成效果。',
'这是第二条Azure语音播报测试,Azure语音合成支持连续多条播报。',
'这是第三条Azure语音播报测试,可以测试语音合成的流畅度和连贯性。',
'这是第四条Azure语音播报测试,感谢您的使用。',
].obs;
// 事件订阅
StreamSubscription? _eventSubscription;
@override
void onInit() {
super.onInit();
@ -61,127 +56,114 @@ class AzureTtsTestController extends GetxController {
errorMessage.value = '';
isLoading.value = false;
// 监听TTS服务状态
_setupEventListener();
// 监听TTS播放状态
ever(_ttsService.isSpeaking, (bool playing) {
isAudioPlaying.value = playing;
if (!playing && currentPlayingIndex.value >= 0) {
// 如果播放结束,重置当前播放索引
currentPlayingIndex.value = -1;
}
});
// 启动播放状态检查定时器
_startPlaybackCheckTimer();
}
// 设置事件监听
void _setupEventListener() {
_eventSubscription?.cancel();
_eventSubscription = _ttsService.synthesisStream?.listen(_handleEvent);
}
// 处理事件
void _handleEvent(SynthesisEvent event) {
switch (event.type) {
case SynthesisEventType.synthesisStarted:
statusMessage.value = '合成开始';
errorMessage.value = '';
break;
case SynthesisEventType.synthesisCompleted:
statusMessage.value = '合成完成';
errorMessage.value = '';
break;
case SynthesisEventType.synthesisCanceled:
statusMessage.value = '合成取消';
errorMessage.value = event.reason ?? '未知原因';
currentPlayingIndex.value = -1;
break;
case SynthesisEventType.error:
statusMessage.value = '错误';
errorMessage.value = event.error ?? '未知错误';
currentPlayingIndex.value = -1;
break;
default:
statusMessage.value = '事件: ${event.type}';
break;
}
// 强制更新UI
update();
// 初始化TTS服务
_initializeTtsService();
}
/// 更新UI状态
void updateUIState() {
// 强制更新UI
update();
}
// 启动播放状态检查定时器
void _startPlaybackCheckTimer() {
_playbackCheckTimer?.cancel();
_playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
// 更新实际播放状态
final actuallyPlaying = _ttsService.ttsState == TtsState.playing || _ttsService.ttsState == TtsState.synthesizing;
/// 初始化TTS服务
Future<void> _initializeTtsService() async {
try {
isLoading.value = true;
errorMessage.value = '';
statusMessage.value = '正在初始化...';
// 只有状态变化时才更新
if (isAudioPlaying.value != actuallyPlaying) {
isAudioPlaying.value = actuallyPlaying;
// 更新UI
update();
// 初始化TTS服务
final result = await _ttsService.initialize();
if (result) {
// 设置默认语音
await _ttsService.setVoice(selectedVoice.value);
// 设置默认参数
await _ttsService.setSpeechParams(
rate: rate.value,
pitch: pitch.value,
volume: volume.value,
);
statusMessage.value = '已连接';
} else {
statusMessage.value = '连接失败';
errorMessage.value = '初始化TTS服务失败';
}
});
isLoading.value = false;
} catch (e) {
isLoading.value = false;
statusMessage.value = '连接失败';
errorMessage.value = '初始化TTS服务失败: $e';
Logger.error('初始化TTS服务失败: $e');
}
}
/// 连续播放所有文本
Future<void> speakContinuousTexts() async {
if (isLoading.value) {
return;
}
/// 切换语音
Future<void> changeVoice(String voice) async {
try {
isLoading.value = true;
isSpeaking.value = true;
errorMessage.value = '';
final speaker = currentVoice.value;
selectedVoice.value = voice;
// 设置语音参数
_ttsService.currentVoice = speaker;
// 设置语音
final result = await _ttsService.setVoice(voice);
// 连续播放所有文本
for (int i = 0; i < speechTexts.length; i++) {
// 如果播放被中断,退出循环
if (!isSpeaking.value) break;
// 在主线程上更新UI
WidgetsBinding.instance.addPostFrameCallback((_) {
currentPlayingIndex.value = i;
statusMessage.value = '正在播放第 ${i + 1}/${speechTexts.length} 条文本';
update();
});
// 播放当前文本,不等待完成
_ttsService.speak(speechTexts[i]);
// 短暂延迟,确保文本被送入队列
await Future.delayed(const Duration(milliseconds: 300));
}
// 在主线程上更新UI
WidgetsBinding.instance.addPostFrameCallback((_) {
statusMessage.value = '所有文本已送入播放队列';
update();
});
if (result) {
statusMessage.value = '已设置语音: $voice';
} else {
errorMessage.value = '设置语音失败';
}
isLoading.value = false;
} catch (e) {
errorMessage.value = '播放出错';
if (kDebugMode) {
print('连续播放出错: $e');
}
} finally {
isLoading.value = false;
errorMessage.value = '设置语音失败: $e';
Logger.error('设置语音失败: $e');
}
}
/// 设置语音参数
Future<void> setSpeechParams({int? newRate, int? newPitch, int? newVolume}) async {
try {
if (newRate != null) rate.value = newRate;
if (newPitch != null) pitch.value = newPitch;
if (newVolume != null) volume.value = newVolume;
// 设置语音参数
final result = await _ttsService.setSpeechParams(
rate: rate.value,
pitch: pitch.value,
volume: volume.value,
);
// 强制更新UI
update();
if (result) {
statusMessage.value = '已设置语音参数';
} else {
errorMessage.value = '设置语音参数失败';
}
} catch (e) {
errorMessage.value = '设置语音参数失败: $e';
Logger.error('设置语音参数失败: $e');
}
}
/// 更新UI状态
void updateUIState() {
// 强制更新UI
update();
}
/// 播放单条文本
Future<void> playSingle(int index) async {
if (index < 0 || index >= speechTexts.length) {
@ -190,126 +172,108 @@ class AzureTtsTestController extends GetxController {
try {
isSpeaking.value = true;
isAudioPlaying.value = true;
errorMessage.value = '';
currentPlayingIndex.value = index;
statusMessage.value = '准备播放...';
statusMessage.value = '正在合成...';
final text = speechTexts[index];
final speaker = currentVoice.value;
// 设置语音参数
_ttsService.currentVoice = speaker;
// 播放文本
await _ttsService.speak(text);
// 使用异步操作,避免阻塞主线程
Future(() {
// 播放文本,不等待完成
_ttsService.speak(text);
// 在主线程上更新UI
WidgetsBinding.instance.addPostFrameCallback((_) {
statusMessage.value = '文本已送入播放队列';
update();
});
});
statusMessage.value = '合成完成';
} catch (e) {
errorMessage.value = '播放出错';
if (kDebugMode) {
print('播放出错: $e');
statusMessage.value = '错误';
Logger.error('播放出错: $e');
} finally {
isSpeaking.value = false;
if (!_ttsService.isSpeaking.value) {
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
}
}
}
/// 停止播放
void stopSpeaking() {
// 启动播放状态检查定时器
void _startPlaybackCheckTimer() {
_playbackCheckTimer?.cancel();
_playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
// 更新实际播放状态
final actuallyPlaying = _ttsService.isSpeaking.value;
isAudioPlaying.value = actuallyPlaying;
// 如果不再播放,确保重置状态
if (!actuallyPlaying && isSpeaking.value) {
// 播放已完成,重置状态
isSpeaking.value = false;
currentPlayingIndex.value = -1;
// 更新UI
update();
}
});
}
/// 连续播放所有文本
Future<void> speakContinuousTexts() async {
if (isAudioPlaying.value || isLoading.value) {
return;
}
try {
isSpeaking.value = false;
statusMessage.value = '正在停止播放...';
isSpeaking.value = true;
errorMessage.value = '';
statusMessage.value = '开始连续播放';
// 使用异步操作,避免阻塞主线程
Future(() async {
// 停止播放
await _ttsService.stop();
// 在主线程上更新UI
WidgetsBinding.instance.addPostFrameCallback((_) {
// 重置UI状态
currentPlayingIndex.value = -1;
statusMessage.value = '已停止播放';
update();
});
});
// 播放所有文本
await _ttsService.speakMultiple(speechTexts.toList());
statusMessage.value = '连续播放完成';
} catch (e) {
if (kDebugMode) {
print('停止播放出错: $e');
}
errorMessage.value = '停止播放出错';
errorMessage.value = '连续播放出错';
statusMessage.value = '错误';
Logger.error('连续播放出错: $e');
} finally {
isSpeaking.value = false;
}
}
// 设置当前语音
void setVoice(String voiceId) {
currentVoice.value = voiceId;
/// 停止播放
Future<void> stopSpeaking() async {
try {
final result = await _ttsService.stop();
if (result) {
isSpeaking.value = false;
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
statusMessage.value = '已停止';
} else {
errorMessage.value = '停止播放失败';
}
} catch (e) {
errorMessage.value = '停止播放出错';
Logger.error('停止播放出错: $e');
}
}
// 获取当前语音名称
String getCurrentVoiceName() {
final voice = availableVoices.firstWhere(
(v) => v['id'] == currentVoice.value,
orElse: () => {'id': currentVoice.value, 'name': '未知语音'},
);
return voice['name'] ?? '未知语音';
/// 切换启用状态
void toggleEnabled() {
_ttsService.toggleEnabled();
statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用';
}
@override
void onClose() {
// 取消事件订阅
_eventSubscription?.cancel();
// 取消播放状态检查定时器
// 取消定时器
_playbackCheckTimer?.cancel();
// 停止播放
_ttsService.stop();
super.onClose();
}
/// 初始化Azure TTS服务
Future<void> initialize({
String? subscriptionKey,
String? region,
}) async {
try {
isLoading.value = true;
statusMessage.value = '正在初始化Azure TTS...';
if (subscriptionKey == null || region == null) {
statusMessage.value = '尝试从环境变量读取配置...';
}
await _ttsService.initialize(
subscriptionKey: subscriptionKey,
region: region,
);
// 检查是否初始化成功
final isSuccess = _ttsService.isInitialized;
if (isSuccess) {
isSpeaking.value = false;
statusMessage.value = 'Azure TTS初始化成功';
} else {
statusMessage.value = 'Azure TTS初始化失败';
}
} catch (e) {
statusMessage.value = '初始化异常: $e';
errorMessage.value = e.toString();
if (kDebugMode) {
print('初始化异常: $e');
}
} finally {
isLoading.value = false;
// 强制更新UI
update();
}
}
}

263
lib/modules/test/controllers/volcano_tts_test_controller.dart

@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../core/utils/logger.dart';
class VolcanoTtsTestController extends GetxController {
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
// 使用 VolcanoTtsService 替代 VolcanoTtsApiService
final VolcanoTtsService _ttsService = Get.put(VolcanoTtsService());
// 可观察状态
final isSpeaking = false.obs;
@ -44,9 +46,6 @@ class VolcanoTtsTestController extends GetxController {
'这是第四条播报测试,感谢您的使用。',
].obs;
// 事件订阅
StreamSubscription? _eventSubscription;
@override
void onInit() {
super.onInit();
@ -57,24 +56,8 @@ class VolcanoTtsTestController extends GetxController {
errorMessage.value = '';
isLoading.value = false;
// 监听TTS服务状态
ever(_ttsService.isConnected, (bool connected) {
if (connected) {
statusMessage.value = '已连接';
} else {
statusMessage.value = '未连接';
isSpeaking.value = false;
isAudioPlaying.value = false;
}
});
// 监听TTS合成状态
ever(_ttsService.isSynthesizing, (bool synthesizing) {
isSpeaking.value = synthesizing;
});
// 监听TTS播放状态
ever(_ttsService.isPlaying.obs, (bool playing) {
ever(_ttsService.isPlaying, (bool playing) {
isAudioPlaying.value = playing;
if (!playing && currentPlayingIndex.value >= 0) {
// 如果播放结束,重置当前播放索引
@ -82,95 +65,10 @@ class VolcanoTtsTestController extends GetxController {
}
});
// 设置事件监听
_setupEventListener();
// 连接到TTS服务
_connectToTtsService();
// 启动播放状态检查定时器
_startPlaybackCheckTimer();
}
// 设置事件监听
void _setupEventListener() {
_eventSubscription?.cancel();
_eventSubscription = _ttsService.eventStream.listen(_handleEvent);
}
// 处理事件
void _handleEvent(Map<String, dynamic> event) {
final eventType = event['eventType'];
switch (eventType) {
case 'connectionStarted':
statusMessage.value = '连接成功';
errorMessage.value = '';
break;
case 'connectionFailed':
statusMessage.value = '连接失败';
errorMessage.value = event['error'] ?? '未知错误';
break;
case 'sessionStarted':
statusMessage.value = '会话开始';
errorMessage.value = '';
break;
case 'sessionFailed':
statusMessage.value = '会话失败';
errorMessage.value = event['error'] ?? '未知错误';
currentPlayingIndex.value = -1;
break;
case 'ttsSentenceStart':
statusMessage.value = '开始合成';
break;
case 'ttsSentenceEnd':
statusMessage.value = '合成完成';
break;
case 'sessionFinished':
statusMessage.value = '会话结束';
currentPlayingIndex.value = -1;
break;
case 'connectionFinished':
statusMessage.value = '连接关闭';
currentPlayingIndex.value = -1;
break;
case 'error':
statusMessage.value = '错误';
errorMessage.value = event['error'] ?? '未知错误';
break;
case 'disconnected':
statusMessage.value = '连接已断开';
currentPlayingIndex.value = -1;
break;
default:
statusMessage.value = '事件: $eventType';
break;
}
// 强制更新UI
update();
}
// 连接到TTS服务
Future<void> _connectToTtsService() async {
try {
isLoading.value = true;
statusMessage.value = '正在连接...';
final success = await _ttsService.connect();
if (!success) {
statusMessage.value = '连接失败';
errorMessage.value = '无法连接到火山语音服务';
}
} catch (e) {
statusMessage.value = '连接错误';
errorMessage.value = e.toString();
} finally {
isLoading.value = false;
}
}
/// 更新UI状态
void updateUIState() {
// 强制更新UI
@ -188,36 +86,25 @@ class VolcanoTtsTestController extends GetxController {
isAudioPlaying.value = true;
errorMessage.value = '';
currentPlayingIndex.value = index;
statusMessage.value = '正在合成...';
final text = speechTexts[index];
final speaker = currentVoice.value;
// 确保TTS服务已连接
if (!_ttsService.isConnected.value) {
await _connectToTtsService();
}
// 开始会话
await _ttsService.startSession(speaker);
// 播放文本
await _ttsService.speak(text, speaker: speaker);
await _ttsService.speak(text);
// 结束会话
await _ttsService.endSession();
// 确保播放列表被清空
await _ttsService.stop();
statusMessage.value = '合成完成';
} catch (e) {
errorMessage.value = '播放出错';
if (kDebugMode) {
print('播放出错: $e');
}
statusMessage.value = '错误';
Logger.error('播放出错: $e');
} finally {
isSpeaking.value = false;
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
if (!_ttsService.isPlaying.value) {
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
}
}
}
@ -226,19 +113,16 @@ class VolcanoTtsTestController extends GetxController {
_playbackCheckTimer?.cancel();
_playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
// 更新实际播放状态
final actuallyPlaying = _ttsService.isPlaying;
final actuallyPlaying = _ttsService.isPlaying.value;
isAudioPlaying.value = actuallyPlaying;
// 如果不再播放,确保重置状态
if (!actuallyPlaying && isSpeaking.value) {
// 检查是否真的播放完成了
if (!_ttsService.isSynthesizing.value && currentPlayingIndex.value >= 0) {
// 播放已完成,重置状态
isSpeaking.value = false;
currentPlayingIndex.value = -1;
// 更新UI
update();
}
// 播放已完成,重置状态
isSpeaking.value = false;
currentPlayingIndex.value = -1;
// 更新UI
update();
}
});
}
@ -250,119 +134,60 @@ class VolcanoTtsTestController extends GetxController {
}
try {
isLoading.value = true;
isSpeaking.value = true;
isAudioPlaying.value = true;
errorMessage.value = '';
statusMessage.value = '开始连续播放';
// 确保TTS服务已连接
if (!_ttsService.isConnected.value) {
await _connectToTtsService();
}
final speaker = currentVoice.value;
// 连接所有文本
final allText = speechTexts.join('');
// 停止之前的播放
await _ttsService.stop();
// 播放所有文本
await _ttsService.speak(allText);
// 开始会话
await _ttsService.startSession(speaker);
// 连续播放所有文本
for (int i = 0; i < speechTexts.length; i++) {
// 如果播放被中断,退出循环
if (!isSpeaking.value) break;
currentPlayingIndex.value = i;
statusMessage.value = '正在播放第 ${i + 1}/${speechTexts.length} 条文本';
// 播放当前文本
await _ttsService.speak(speechTexts[i], speaker: speaker);
}
// 结束会话
await _ttsService.endSession();
statusMessage.value = '播放完成';
statusMessage.value = '连续播放完成';
} catch (e) {
errorMessage.value = '播放出错';
if (kDebugMode) {
print('连续播放出错: $e');
}
// 尝试结束会话,忽略可能的错误
try {
await _ttsService.endSession();
} catch (_) {
// 忽略结束会话时的错误
}
errorMessage.value = '连续播放出错';
statusMessage.value = '错误';
Logger.error('连续播放出错: $e');
} finally {
currentPlayingIndex.value = -1;
isSpeaking.value = false;
isLoading.value = false;
// 确保最终状态正确
isAudioPlaying.value = false;
// 强制更新UI
update();
}
}
/// 停止播放
void stopSpeaking() async {
Future<void> stopSpeaking() async {
try {
isSpeaking.value = false;
statusMessage.value = '正在停止播放...';
// 停止播放
await _ttsService.stop();
// 结束会话
await _ttsService.endSession();
// 重置UI状态
isSpeaking.value = false;
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
statusMessage.value = '已停止播放';
statusMessage.value = '已停止';
} catch (e) {
if (kDebugMode) {
print('停止播放出错: $e');
}
errorMessage.value = '停止播放出错';
} finally {
isSpeaking.value = false;
isLoading.value = false;
isAudioPlaying.value = false;
Logger.error('停止播放出错: $e');
}
}
// 设置当前语音
void setVoice(String voiceId) {
/// 切换语音
void changeVoice(String voiceId) {
currentVoice.value = voiceId;
// VolcanoTtsService 不需要显式设置发音人,在 speak 时会使用环境变量中的默认发音人
}
// 获取当前语音名称
String getCurrentVoiceName() {
final voice = availableVoices.firstWhere(
(v) => v['id'] == currentVoice.value,
orElse: () => {'id': currentVoice.value, 'name': '未知语音'},
);
return voice['name'] ?? '未知语音';
/// 切换启用状态
void toggleEnabled() {
_ttsService.isEnabled.toggle();
statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用';
}
@override
void onClose() {
// 取消事件订阅
_eventSubscription?.cancel();
// 取消播放状态检查定时器
// 取消定时器
_playbackCheckTimer?.cancel();
// 断开TTS服务连接
_ttsService.disconnect();
// 停止播放
_ttsService.stop();
super.onClose();
}

345
lib/modules/test/views/azure_asr_test_view.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/azure_asr_test_controller.dart';
import '../../../data/services/azure_asr_service.dart';
class AzureAsrTestView extends GetView<AzureAsrTestController> {
const AzureAsrTestView({Key? key}) : super(key: key);
@ -9,224 +10,164 @@ class AzureAsrTestView extends GetView<AzureAsrTestController> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Azure 语音识别测试'),
centerTitle: true,
title: const Text('Azure语音识别测试'),
actions: [
_buildLanguageDropdown(),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 识别模式选择
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'识别模式',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Obx(() => Row(
children: [
Expanded(
child: RadioListTile<bool>(
title: const Text('一次性识别'),
value: false,
groupValue: controller.isContinuous.value,
onChanged: (value) {
if (value != null) {
controller.isContinuous.value = value;
}
},
),
),
Expanded(
child: RadioListTile<bool>(
title: const Text('连续识别'),
value: true,
groupValue: controller.isContinuous.value,
onChanged: (value) {
if (value != null) {
controller.isContinuous.value = value;
}
},
),
),
],
)),
],
),
),
),
_buildStatusCard(),
const SizedBox(height: 16),
// 录音控制
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'录音控制',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Center(
child: Obx(() => controller.isListening.value
? ElevatedButton.icon(
onPressed: () => controller.stopListening(),
icon: const Icon(Icons.stop),
label: const Text('停止录音'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
)
: ElevatedButton.icon(
onPressed: () => controller.startListening(),
icon: const Icon(Icons.mic),
label: const Text('开始录音'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
),
),
],
),
_buildRecognitionResultCard(),
const SizedBox(height: 16),
_buildControlButtons(context),
],
),
),
);
}
// 构建状态卡片
Widget _buildStatusCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'状态信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 识别结果
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'识别结果',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => controller.clearResults(),
tooltip: '清空结果',
),
],
),
const SizedBox(height: 8),
Expanded(
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4.0),
),
child: Obx(() => ListView.builder(
itemCount: controller.recognitionResults.length,
itemBuilder: (context, index) {
final result = controller.recognitionResults[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${index + 1}. ',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Expanded(
child: Text(result),
),
],
),
);
},
)),
),
),
],
),
const SizedBox(height: 8),
Obx(() => Text('识别状态: ${_getStateText(controller.asrState.value)}')),
Obx(() => Text('是否正在监听: ${controller.isListening.value ? "是" : "否"}')),
Obx(() => Text('当前语言: ${controller.selectedLanguage.value}')),
],
),
),
);
}
// 构建识别结果卡片
Widget _buildRecognitionResultCard() {
return Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'识别结果',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
// 状态显示
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'状态',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Obx(() => Text(
controller.isListening.value
? '正在录音...'
: '就绪',
style: TextStyle(
color: controller.isListening.value
? Colors.green
: Colors.grey,
fontWeight: FontWeight.bold,
const SizedBox(height: 8),
const Text('部分识别结果:'),
Obx(() => Text(
controller.partialText.value,
style: const TextStyle(color: Colors.blue),
)),
const SizedBox(height: 16),
const Text('最终识别结果:'),
Expanded(
child: Obx(() => SingleChildScrollView(
child: Text(
controller.recognizedText.value,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
)),
const SizedBox(height: 8),
Obx(() => controller.errorMessage.value.isNotEmpty
? Text(
'错误: ${controller.errorMessage.value}',
style: const TextStyle(
color: Colors.red,
),
)
: const SizedBox.shrink()),
],
),
),
),
],
],
),
),
),
);
}
// 构建控制按钮
Widget _buildControlButtons(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Obx(() => ElevatedButton(
onPressed: controller.isListening.value
? null
: controller.startSimpleRecognition,
child: const Text('简单识别'),
)),
),
const SizedBox(width: 16),
Expanded(
child: Obx(() => ElevatedButton(
onPressed: controller.toggleContinuousRecognition,
style: ElevatedButton.styleFrom(
backgroundColor: controller.isListening.value
? Colors.red
: Theme.of(context).primaryColor,
),
child: Text(
controller.isListening.value ? '停止连续识别' : '开始连续识别',
),
)),
),
],
);
}
// 构建语言下拉菜单
Widget _buildLanguageDropdown() {
return Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Obx(() => DropdownButton<String>(
value: controller.selectedLanguage.value,
onChanged: controller.isListening.value
? null
: (value) {
if (value != null) {
controller.changeLanguage(value);
}
},
items: controller.availableLanguages
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
)),
);
}
// 获取状态文本
String _getStateText(AzureAsrState state) {
switch (state) {
case AzureAsrState.notInitialized:
return '未初始化';
case AzureAsrState.initialized:
return '已初始化';
case AzureAsrState.listening:
return '正在监听';
case AzureAsrState.notListening:
return '未监听';
case AzureAsrState.error:
return '错误';
default:
return '未知';
}
}
}

137
lib/modules/test/views/azure_tts_test_view.dart

@ -11,23 +11,12 @@ class AzureTtsTestView extends GetView<AzureTtsTestController> {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在下一帧渲染完成后调用
controller.updateUIState();
// 尝试从环境变量初始化
controller.initialize();
});
return Scaffold(
appBar: AppBar(
title: const Text('Azure 语音合成测试'),
title: const Text('Azure语音合成测试'),
centerTitle: true,
actions: [
// 添加初始化按钮
IconButton(
icon: const Icon(Icons.refresh),
tooltip: '从环境变量初始化',
onPressed: () => controller.initialize(),
),
],
),
body: SingleChildScrollView(
child: Padding(
@ -35,6 +24,11 @@ class AzureTtsTestView extends GetView<AzureTtsTestController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 语音选择
_buildVoiceSelectionCard(),
const SizedBox(height: 16),
// 播放控制
Card(
child: Padding(
@ -162,51 +156,6 @@ class AzureTtsTestView extends GetView<AzureTtsTestController> {
const SizedBox(height: 16),
// 语音选择
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'语音选择',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: controller.availableVoices.map((voice) {
final id = voice['id'] ?? '';
final name = voice['name'] ?? '';
return Obx(() => ChoiceChip(
label: Text(name),
selected: controller.currentVoice.value == id,
onSelected: (selected) {
if (selected) {
controller.setVoice(id);
}
},
selectedColor: Colors.blue,
labelStyle: TextStyle(
color: controller.currentVoice.value == id
? Colors.white
: Colors.black,
),
));
}).toList(),
),
],
),
),
),
const SizedBox(height: 16),
// 状态显示
Card(
child: Padding(
@ -277,4 +226,78 @@ class AzureTtsTestView extends GetView<AzureTtsTestController> {
),
);
}
// 构建语音选择卡片
Widget _buildVoiceSelectionCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'语音选择',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 语音选择
Obx(() {
if (controller.isLoadingVoices.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
);
}
if (controller.availableVoices.isEmpty) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Text('未找到可用的语音', style: TextStyle(color: Colors.red)),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('选择语音:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.availableVoices.length,
itemBuilder: (context, index) {
final voice = controller.availableVoices[index];
final isSelected = controller.selectedVoice.value == voice;
return RadioListTile<String>(
title: Text(voice),
subtitle: Text(voice),
value: voice,
groupValue: controller.selectedVoice.value,
onChanged: (value) {
if (value != null) {
controller.changeVoice(value);
}
},
selected: isSelected,
activeColor: Colors.blue,
dense: true,
);
},
),
],
);
}),
],
),
),
);
}
}

9
lib/modules/translation/bindings/translation_binding.dart

@ -1,11 +1,18 @@
import 'package:get/get.dart';
import '../controllers/translation_controller.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/azure_tts_service.dart';
import '../../../data/services/volcano_translation_service.dart';
class TranslationBinding implements Bindings {
@override
void dependencies() {
// 注册服务
Get.put(AzureAsrService(), permanent: true);
Get.put(AzureTtsService(), permanent: true);
Get.put(VolcanoTranslationService(), permanent: true);
// Register the controller
// 注册控制器
Get.put(TranslationController());
}
}

183
lib/modules/translation/controllers/translation_controller.dart

@ -3,9 +3,9 @@ import 'package:get/get.dart';
import 'dart:async';
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/azure_tts_service.dart';
import '../../../core/utils/logger.dart';
class TranslationController extends GetxController {
@ -13,7 +13,7 @@ class TranslationController extends GetxController {
final AzureAsrService _asrService = Get.find<AzureAsrService>();
final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
final GetStorage _storage = GetStorage();
// Storage key
@ -86,6 +86,12 @@ class TranslationController extends GetxController {
// Initialize the translation service
_initTranslationService();
// 初始化 Azure ASR 服务
_initAzureAsrService();
// 监听部分识别结果和识别状态
_setupListeners();
// 添加后处理,确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
// 延迟执行滚动操作,确保视图已完全渲染
@ -93,9 +99,6 @@ class TranslationController extends GetxController {
_scrollToBottom();
});
});
// Start voice recognition automatically when the page is loaded
startRecognition();
}
@override
@ -120,6 +123,20 @@ class TranslationController extends GetxController {
}
}
// 初始化 Azure ASR 服务
Future<void> _initAzureAsrService() async {
try {
// 设置识别语言
final sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
final recognitionLanguage = _getRecognitionLanguage(sourceCode);
await _asrService.initialize(language: recognitionLanguage);
Logger.info('Azure ASR 服务初始化成功');
} catch (e) {
Logger.error('初始化 Azure ASR 服务失败: $e');
}
}
// Toggle TTS feature
void toggleTts() {
isTtsEnabled.toggle();
@ -144,59 +161,69 @@ class TranslationController extends GetxController {
isFirstInSession: true, // 标记为会话中的第一条
);
// Start the voice recognition service
final success = await _asrService.startContinuousRecognition();
if (success) {
isRecognizing.value = true;
// 设置识别语言
final sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
final recognitionLanguage = _getRecognitionLanguage(sourceCode);
// 确保使用正确的语言
await _asrService.setRecognitionLanguage(recognitionLanguage);
// 开始连续语音识别
final recognitionStream = await _asrService.startContinuousRecognition();
isRecognizing.value = true;
// Reset the current item
if (currentItem.value != null) {
currentItem.value!.sourceText = '';
currentItem.value!.translatedText = '';
}
// Reset the current item
if (currentItem.value != null) {
currentItem.value!.sourceText = '';
currentItem.value!.translatedText = '';
// 监听识别结果
_recognitionSubscription?.cancel();
_recognitionSubscription = recognitionStream.listen((event) {
if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) {
handleFinalResult(event.text);
} else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) {
handleIntermediateResult(event.text);
} else if (event.type == RecognitionEventType.error) {
isRecognizing.value = false;
Logger.error('语音识别错误: ${event.error}');
} else if (event.type == RecognitionEventType.canceled) {
isRecognizing.value = false;
Logger.error('语音识别取消: ${event.error}');
}
// Listen for recognition events
_recognitionSubscription =
_asrService.recognitionStream?.listen((event) {
switch (event.type) {
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
handleFinalResult(event.text);
}
break;
case RecognitionEventType.recognizing:
// Update the UI with intermediate results
if (event.text.isNotEmpty) {
handleIntermediateResult(event.text);
}
break;
case RecognitionEventType.error:
// 错误处理,停止语音识别
Logger.error('语音识别错误: ${event.error}');
// 停止语音识别
isRecognizing.value = false;
_asrService.stopContinuousRecognition();
break;
default:
break;
}
});
} else {
throw Exception('Failed to start continuous recognition');
}
});
} catch (e) {
isRecognizing.value = false;
Get.snackbar(
'Error',
'Failed to start voice recognition: ${e.toString()}',
'启动语音识别失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// 将语言代码转换为识别语言代码
String _getRecognitionLanguage(String languageCode) {
// 语言代码到识别语言代码的映射
final Map<String, String> languageToRecognitionCode = {
'zh': 'zh-CN',
'en': 'en-US',
'ja': 'ja-JP',
'ko': 'ko-KR',
'fr': 'fr-FR',
'de': 'de-DE',
'es': 'es-ES',
'it': 'it-IT',
'ru': 'ru-RU',
'pt': 'pt-BR',
'ar': 'ar-EG',
'th': 'th-TH',
};
return languageToRecognitionCode[languageCode] ?? 'zh-CN';
}
// Stop voice recognition
Future<void> stopRecognition() async {
if (!isRecognizing.value) return;
@ -248,14 +275,24 @@ class TranslationController extends GetxController {
// 防抖处理翻译请求
_translationDebounceTimer?.cancel();
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
if (currentItem.value != null &&
currentItem.value!.sourceText.isNotEmpty) {
translateText(currentItem.value!.sourceText);
}
translateCurrentItem(isIntermediate: true);
});
}
// 滚动到底部显示最新内容
_scrollToBottom();
// 翻译当前项
Future<void> translateCurrentItem({bool isIntermediate = false}) async {
if (currentItem.value == null || currentItem.value!.sourceText.isEmpty) {
return;
}
final sourceText = currentItem.value!.sourceText;
final translationResult = await translateText(sourceText);
if (translationResult != null && isIntermediate) {
// 如果是中间结果,不播放TTS
// 滚动到底部显示最新内容
_scrollToBottom();
}
}
// Handle final recognition result
@ -386,12 +423,41 @@ class TranslationController extends GetxController {
if (text.isEmpty) return;
try {
await _ttsService.speakOnce(text);
// 获取目标语言代码
final targetCode = supportedLanguages[targetLanguage.value] ?? 'en';
// 设置合适的语音
final voiceName = _getTtsVoiceName(targetCode);
await _ttsService.setVoice(voiceName);
// 播放文本
await _ttsService.speak(text);
} catch (e) {
Get.snackbar('Error', '播放翻译失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM);
}
}
// 获取TTS语音名称
String _getTtsVoiceName(String languageCode) {
// 语言代码到TTS语音名称的映射
final Map<String, String> languageToVoiceName = {
'zh': 'zh-CN-XiaoxiaoNeural',
'en': 'en-US-AriaNeural',
'ja': 'ja-JP-NanamiNeural',
'ko': 'ko-KR-SunHiNeural',
'fr': 'fr-FR-DeniseNeural',
'de': 'de-DE-KatjaNeural',
'es': 'es-ES-ElviraNeural',
'it': 'it-IT-ElsaNeural',
'ru': 'ru-RU-SvetlanaNeural',
'pt': 'pt-BR-FranciscaNeural',
'ar': 'ar-EG-SalmaNeural',
'th': 'th-TH-PremwadeeNeural',
};
return languageToVoiceName[languageCode] ?? 'en-US-AriaNeural';
}
// 停止所有操作(语音识别和TTS)
Future<void> stopAll() async {
@ -510,6 +576,13 @@ class TranslationController extends GetxController {
translationHistory.clear();
_storage.remove(_historyKey);
}
// 设置监听器
void _setupListeners() {
// 在新版本中,我们通过recognitionStream直接监听事件
// 这部分逻辑已经在startRecognition方法中实现
Logger.info('语音识别事件监听已设置');
}
}
// Model class for translation items

2
pubspec.yaml

@ -29,6 +29,7 @@ dependencies:
crypto: ^3.0.3
flutter_tts: ^4.2.2
logger: ^2.0.2+1
# azure_speech_recognition_null_safety: ^0.9.6
window_size:
git:
url: https://github.com/google/flutter-desktop-embedding.git
@ -41,6 +42,7 @@ dependencies:
share_plus: ^10.0.0
image_picker: ^1.1.2
speech_to_text: ^7.0.0
audio_waveforms: ^1.3.0
dev_dependencies:
flutter_test:

Loading…
Cancel
Save