lxm 2 years ago
parent
commit
1fe5f606c1
  1. 13
      lib/core/bindings/initial_binding.dart
  2. 24
      lib/core/themes/theme_service.dart
  3. 2
      lib/core/utils/app_lifecycle_manager.dart
  4. 105
      lib/data/services/asr_service.dart
  5. 110
      lib/data/services/audio_processing_service.dart
  6. 156
      lib/data/services/audio_service.dart
  7. 793
      lib/data/services/azure_flutter_tts_service.dart
  8. 491
      lib/data/services/azure_tts_service.dart
  9. 377
      lib/data/services/background_agent_service.dart
  10. 142
      lib/data/services/flutter_asr_service.dart
  11. 367
      lib/data/services/flutter_tts_service.dart
  12. 352
      lib/data/services/my_audio_handler.dart
  13. 224
      lib/data/services/notification_service.dart
  14. 108
      lib/data/services/speech_factory.dart
  15. 123
      lib/data/services/speech_impl/azure_asr_service.dart
  16. 570
      lib/data/services/speech_impl/azure_tts_service.dart
  17. 439
      lib/data/services/speech_impl/flutter_asr_service.dart
  18. 484
      lib/data/services/speech_impl/flutter_tts_service.dart
  19. 807
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  20. 2
      lib/data/services/speech_impl/volcano_tts_api_service.dart
  21. 83
      lib/data/services/tts_service.dart
  22. 6
      lib/data/services/voice_interaction_service.dart
  23. 1003
      lib/data/services/volcano_asr_api_service.dart
  24. 743
      lib/data/services/volcano_tts_service.dart
  25. 22
      lib/modules/chat/bindings/chat_binding.dart
  26. 17
      lib/modules/chat/controllers/chat_controller.dart
  27. 2
      lib/modules/chat/views/chat_view.dart
  28. 1
      lib/modules/profile/views/profile_view.dart
  29. 39
      lib/modules/settings/controllers/settings_controller.dart
  30. 483
      lib/modules/settings/views/settings_view.dart
  31. 16
      lib/modules/test/bindings/azure_asr_test_binding.dart
  32. 10
      lib/modules/test/bindings/azure_tts_test_binding.dart
  33. 16
      lib/modules/test/bindings/flutter_asr_test_binding.dart
  34. 10
      lib/modules/test/bindings/flutter_tts_test_binding.dart
  35. 25
      lib/modules/test/bindings/test_binding.dart
  36. 10
      lib/modules/test/bindings/volcano_asr_test_binding.dart
  37. 298
      lib/modules/test/controllers/azure_asr_test_controller.dart
  38. 279
      lib/modules/test/controllers/azure_tts_test_controller.dart
  39. 154
      lib/modules/test/controllers/flutter_asr_test_controller.dart
  40. 163
      lib/modules/test/controllers/flutter_tts_test_controller.dart
  41. 218
      lib/modules/test/controllers/volcano_asr_test_controller.dart
  42. 194
      lib/modules/test/controllers/volcano_tts_test_controller.dart
  43. 173
      lib/modules/test/views/azure_asr_test_view.dart
  44. 303
      lib/modules/test/views/azure_tts_test_view.dart
  45. 476
      lib/modules/test/views/flutter_asr_test_view.dart
  46. 334
      lib/modules/test/views/flutter_tts_test_view.dart
  47. 234
      lib/modules/test/views/volcano_asr_test_view.dart
  48. 224
      lib/modules/test/views/volcano_tts_test_view.dart
  49. 10
      lib/modules/translation/bindings/translation_binding.dart
  50. 14
      lib/modules/translation/controllers/translation_controller.dart
  51. 53
      lib/routes/app_pages.dart
  52. 1
      pubspec.yaml

13
lib/core/bindings/initial_binding.dart

@ -1,6 +1,5 @@
import 'package:get/get.dart';
import '../../data/services/azure_asr_service.dart';
import '../../data/services/azure_tts_service.dart';
import '../../data/services/speech_factory.dart';
import '../../data/services/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/voice_interaction_service.dart';
@ -18,11 +17,9 @@ class InitialBinding extends Bindings {
// 语言管理器(需要最先初始化)
Get.put<LanguageManager>(LanguageManager(), permanent: true);
// Azure 语音识别服务
Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true);
// Azure TTS 服务 (原生实现)
Get.lazyPut<AzureTtsService>(() => AzureTtsService(), fenix: true);
//初始化语音工厂
Get.put<SpeechFactory>(SpeechFactory(), permanent: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
// 历史记录服务
Get.lazyPut<ChatHistoryService>(() => ChatHistoryService(), fenix: true);
@ -38,5 +35,7 @@ class InitialBinding extends Bindings {
// 经典蓝牙服务
Get.lazyPut<ClassicBluetoothService>(() => ClassicBluetoothService(), fenix: true);
}
}

24
lib/core/themes/theme_service.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../core/utils/logger.dart';
class ThemeService {
final _box = GetStorage();
@ -9,20 +10,29 @@ class ThemeService {
/// 获取主题模式
ThemeMode getThemeMode() {
final isDarkMode = isSavedDarkMode();
print('获取保存的主题: ${isDarkMode ? "暗黑" : "浅色"}');
Logger.info('获取保存的主题: ${isDarkMode ? "暗黑" : "浅色"}');
return isDarkMode ? ThemeMode.dark : ThemeMode.light;
}
/// 是否是暗黑模式
bool isSavedDarkMode() {
final value = _box.read(_key);
print('从存储读取主题设置: $value');
return value ?? false;
try {
final value = _box.read<bool>(_key);
Logger.info('从存储读取主题设置: $value');
return value ?? false;
} catch (e) {
Logger.error('读取主题设置失败: $e');
return false;
}
}
/// 保存主题模式到本地存储
void saveThemeMode(bool isDarkMode) {
print('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}');
_box.write(_key, isDarkMode);
Future<void> saveThemeMode(bool isDarkMode) async {
try {
Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}');
await _box.write(_key, isDarkMode);
} catch (e) {
Logger.error('保存主题设置失败: $e');
}
}
}

2
lib/core/utils/app_lifecycle_manager.dart

@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../data/services/volcano_asr_api_service.dart';
import '../../data/services/azure_flutter_tts_service.dart';
import 'logger.dart';
/// 应用生命周期管理器

105
lib/data/services/asr_service.dart

@ -0,0 +1,105 @@
import 'dart:async';
/// 语音识别服务接口
abstract class AsrService {
/// 支持的语言
List<String> get supportedLanguages;
/// 初始化语音识别服务
Future<bool> initialize({
List<String>? supportedLanguages,
});
/// 执行一次性语音识别
///
/// 返回识别的最终结果
Future<RecognitionEvent> recognizeOnce();
/// 开始连续语音识别
///
/// 返回一个包含识别事件的流
Future<Stream<RecognitionEvent>> startContinuousRecognition();
/// 停止连续语音识别
Future<bool> stopContinuousRecognition();
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive();
/// 释放资源
Future<void> dispose();
}
/// 识别事件类型
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 识别错误
error,
}
/// 识别事件
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
/// 检测到的语言
final String detectedLanguage;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
RecognitionEvent({
required this.type,
this.text = '',
this.detectedLanguage = '',
this.error = '',
});
/// 创建最终结果事件的快捷构造函数
factory RecognitionEvent.finalResult({
required String text,
String detectedLanguage = '',
}) {
return RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
detectedLanguage: detectedLanguage,
);
}
/// 创建错误事件的快捷构造函数
factory RecognitionEvent.error(String errorMessage) {
return RecognitionEvent(
type: RecognitionEventType.error,
error: errorMessage,
);
}
/// 检查是否为最终结果
bool get isFinalResult => type == RecognitionEventType.finalResult;
/// 检查是否为错误
bool get isError => type == RecognitionEventType.error || type == RecognitionEventType.canceled;
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}';
}
}

110
lib/data/services/audio_processing_service.dart

@ -1,110 +0,0 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
/// 音频处理服务
///
/// 提供回音消除、降噪和语音活动检测等功能
class AudioProcessingService extends GetxService {
static const MethodChannel _channel = MethodChannel('com.deep_voice.audio_processing');
// 状态变量
final _isEchoCancellationEnabled = false.obs;
final _isNoiseSuppressionEnabled = false.obs;
final _isVoiceActivityDetectionEnabled = false.obs;
// 获取可观察状态
RxBool get isEchoCancellationEnabled => _isEchoCancellationEnabled;
RxBool get isNoiseSuppressionEnabled => _isNoiseSuppressionEnabled;
RxBool get isVoiceActivityDetectionEnabled => _isVoiceActivityDetectionEnabled;
AudioProcessingService() {
_initPlatformState();
}
/// 初始化平台状态
Future<void> _initPlatformState() async {
try {
final bool isEchoCancellationAvailable = await _channel.invokeMethod('isEchoCancellationAvailable') ?? false;
final bool isNoiseSuppressionAvailable = await _channel.invokeMethod('isNoiseSuppressionAvailable') ?? false;
Logger.info('回音消除可用: $isEchoCancellationAvailable');
Logger.info('噪声抑制可用: $isNoiseSuppressionAvailable');
} catch (e) {
Logger.error('初始化音频处理服务失败: $e');
}
}
/// 为指定的音频会话ID启用回音消除
Future<bool> enableEchoCancellation(int audioSessionId) async {
try {
final bool result = await _channel.invokeMethod('enableEchoCancellation', {
'audioSessionId': audioSessionId,
}) ?? false;
_isEchoCancellationEnabled.value = result;
Logger.info('启用回音消除${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('启用回音消除失败: $e');
return false;
}
}
/// 禁用回音消除
Future<bool> disableEchoCancellation() async {
try {
final bool result = await _channel.invokeMethod('disableEchoCancellation') ?? false;
_isEchoCancellationEnabled.value = !result;
Logger.info('禁用回音消除${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('禁用回音消除失败: $e');
return false;
}
}
/// 为指定的音频会话ID启用噪声抑制
Future<bool> enableNoiseSuppression(int audioSessionId) async {
try {
final bool result = await _channel.invokeMethod('enableNoiseSuppression', {
'audioSessionId': audioSessionId,
}) ?? false;
_isNoiseSuppressionEnabled.value = result;
Logger.info('启用噪声抑制${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('启用噪声抑制失败: $e');
return false;
}
}
/// 禁用噪声抑制
Future<bool> disableNoiseSuppression() async {
try {
final bool result = await _channel.invokeMethod('disableNoiseSuppression') ?? false;
_isNoiseSuppressionEnabled.value = !result;
Logger.info('禁用噪声抑制${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('禁用噪声抑制失败: $e');
return false;
}
}
/// 获取当前录音的音频会话ID
Future<int?> getAudioSessionId() async {
try {
final int? sessionId = await _channel.invokeMethod('getAudioSessionId');
Logger.info('获取音频会话ID: $sessionId');
return sessionId;
} catch (e) {
Logger.error('获取音频会话ID失败: $e');
return null;
}
}
}

156
lib/data/services/audio_service.dart

@ -1,156 +0,0 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:synchronized/synchronized.dart';
import 'my_audio_handler.dart';
class AudioServiceManager extends GetxService {
static AudioServiceManager get to => Get.find();
MyAudioHandler? _audioHandler;
bool _isInitialized = false;
final _lock = Lock();
// 添加一个标志,表示是否已经尝试过初始化
bool _hasAttemptedInit = false;
// 添加一个计时器,用于定期检查服务状态
Timer? _serviceCheckTimer;
Future<AudioServiceManager> init() async {
print('初始化 AudioServiceManager');
if (_isInitialized) {
print('AudioServiceManager 已经初始化');
return this;
}
// 如果已经尝试过初始化但失败了,不再重试
if (_hasAttemptedInit) {
print('AudioServiceManager 之前初始化失败,尝试重新初始化');
// 重置标志,允许重新初始化
_hasAttemptedInit = false;
}
_hasAttemptedInit = true;
try {
// 使用锁确保初始化的原子性
await _lock.synchronized(() async {
if (_isInitialized) return;
// 使用 AudioService.init 初始化音频服务
_audioHandler = await AudioService.init<MyAudioHandler>(
builder: () => MyAudioHandler(),
config: AudioServiceConfig(
androidNotificationChannelId: 'com.example.deep_voice.channel.audio',
androidNotificationChannelName: 'Deep Voice Audio Service',
// 使用应用图标作为通知图标,确保这个图标存在
androidNotificationIcon: 'mipmap/ic_launcher',
// 修复配置冲突:当androidStopForegroundOnPause为true时,androidNotificationOngoing才能为true
androidNotificationOngoing: false,
// 设置为true,允许在暂停时停止前台服务,避免与androidNotificationOngoing冲突
androidStopForegroundOnPause: true,
androidShowNotificationBadge: true,
notificationColor: Colors.blue,
fastForwardInterval: const Duration(seconds: 10),
rewindInterval: const Duration(seconds: 10),
preloadArtwork: false,
),
);
// 注册自定义音频处理器
await Get.put(_audioHandler!, permanent: true);
_isInitialized = true;
print('AudioServiceManager 初始化成功');
// 启动定期检查服务状态的计时器
_startServiceCheckTimer();
});
return this;
} catch (e) {
print('AudioServiceManager初始化失败: $e');
_isInitialized = false; // 确保失败时重置状态
// 延迟5秒后尝试重新初始化
Future.delayed(Duration(seconds: 5), () {
print('尝试重新初始化 AudioServiceManager');
_hasAttemptedInit = false;
init();
});
return this; // 即使失败也返回实例,避免空指针异常
}
}
// 启动定期检查服务状态的计时器
void _startServiceCheckTimer() {
_serviceCheckTimer?.cancel();
_serviceCheckTimer = Timer.periodic(Duration(minutes: 5), (timer) {
_checkAndRestartServiceIfNeeded();
});
}
// 检查服务状态,如果需要则重启服务
Future<void> _checkAndRestartServiceIfNeeded() async {
if (!_isInitialized || _audioHandler == null) {
print('服务检查:AudioServiceManager 未初始化,尝试重新初始化');
_hasAttemptedInit = false;
await init();
return;
}
print('服务检查:AudioServiceManager 已初始化,服务状态: $_isInitialized');
}
// 获取音频处理器
MyAudioHandler? get audioHandler => _audioHandler;
// 发送媒体按钮事件
Future<void> sendMediaButtonEvent() async {
if (_audioHandler != null) {
try {
print('发送媒体按钮事件');
await _audioHandler!.customAction('media_button');
} catch (e) {
print('发送媒体按钮事件失败: $e');
// 如果发送失败,尝试重新初始化服务
_isInitialized = false;
_hasAttemptedInit = false;
await init();
}
} else {
print('AudioHandler 未初始化,无法发送媒体按钮事件');
// 尝试重新初始化
_isInitialized = false;
_hasAttemptedInit = false;
await init();
}
}
// 手动触发媒体按钮事件(用于测试)
Future<void> triggerMediaButtonEvent() async {
print('手动触发媒体按钮事件');
await sendMediaButtonEvent();
}
@override
void onClose() async {
_isInitialized = false;
_serviceCheckTimer?.cancel();
// 停止现有的音频处理器
if (_audioHandler != null) {
try {
await _audioHandler!.stop();
} catch (e) {
print('停止 AudioHandler 失败: $e');
}
_audioHandler = null;
}
super.onClose();
}
}

793
lib/data/services/azure_flutter_tts_service.dart

@ -1,793 +0,0 @@
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,
});
}
}
}

491
lib/data/services/azure_tts_service.dart

@ -1,491 +0,0 @@
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;
}
/// 音频输出设备类型
enum AudioOutputType {
/// 扬声器
speaker,
/// 听筒
earpiece,
/// 自动选择(有耳机用耳机,没有则用听筒)
auto
}
/// 微软 Text-to-Speech 服务
///
/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。
class AzureTtsService extends GetxService {
static final AzureTtsService to = Get.put(AzureTtsService());
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;
AudioOutputType _currentAudioOutputType = AudioOutputType.auto;
// 语音合成队列
final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false;
bool _isSpeaking = false;
// 流式输出缓冲区
String _streamBuffer = '';
// 可观察状态
final isEnabled = true.obs;
final isSpeaking = false.obs;
final audioOutputType = AudioOutputType.auto.obs;
String get currentVoice => _currentVoice;
AudioOutputType get currentAudioOutputType => _currentAudioOutputType;
AzureTtsService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_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,
'region': _serviceRegion,
'language': language,
});
_isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
// 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值
// 只需要确保 Dart 层的状态与原生层一致
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
return result;
} catch (e) {
Logger.error('初始化失败: $e');
return false;
}
}
/// 设置语音
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
if (voiceName == _currentVoice) return true;
try {
final result = await _channel.invokeMethod('setVoice', {
'voiceName': voiceName,
});
if (result) _currentVoice = voiceName;
return result;
} catch (e) {
Logger.error('设置语音失败: $e');
return false;
}
}
/// 设置音频输出设备类型
Future<bool> setAudioOutputType(AudioOutputType outputType) async {
if (!_isInitialized) await initialize();
try {
final String outputTypeStr = outputType.toString().split('.').last;
final result = await _channel.invokeMethod('setAudioOutputType', {
'outputType': outputTypeStr,
});
if (result) {
_currentAudioOutputType = outputType;
audioOutputType.value = outputType;
}
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, AudioOutputType? outputType}) async {
if (!isEnabled.value || text.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
_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, {AudioOutputType? outputType}) async {
if (!isEnabled.value || textChunk.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
// 添加文本到缓冲区
_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({AudioOutputType? outputType}) async {
if (!isEnabled.value || _streamBuffer.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
// 先尝试使用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, AudioOutputType? outputType}) async {
if (!isEnabled.value || texts.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
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<bool> toggleAudioOutputType() async {
AudioOutputType newType;
switch (_currentAudioOutputType) {
case AudioOutputType.speaker:
newType = AudioOutputType.earpiece;
break;
case AudioOutputType.earpiece:
newType = AudioOutputType.auto;
break;
case AudioOutputType.auto:
newType = AudioOutputType.speaker;
break;
}
return await setAudioOutputType(newType);
}
/// 释放资源
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,
});
}

377
lib/data/services/background_agent_service.dart

@ -1,377 +0,0 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
import '../../core/utils/logger.dart';
import '../providers/agent_provider.dart';
import '../models/agent.dart';
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 'azure_flutter_tts_service.dart';
import 'chat_history_service.dart';
/// 后台代理服务,用于在应用处于后台时处理AI交互
class BackgroundAgentService extends GetxService {
// 单例实例
static BackgroundAgentService? _instance;
// 单例工厂方法
factory BackgroundAgentService() {
_instance ??= BackgroundAgentService._internal();
return _instance!;
}
// 私有构造函数
BackgroundAgentService._internal() {
Logger.info('BackgroundAgentService 已创建');
}
// 服务实例
final VolcanoAsrApiService _asrService = Get.find<VolcanoAsrApiService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final AzureFlutterTtsService _ttsService = Get.find<AzureFlutterTtsService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
// 状态控制
// isProcessing: 表示整个代理交互会话是否处于活跃状态(包括语音识别、AI处理和TTS播放)
final RxBool isProcessing = false.obs;
final RxString recognizedText = ''.obs;
// 流式响应控制
StreamSubscription? _recognitionSubscription;
StreamSubscription? _aiResponseSubscription;
// 定时器,用于检测无活动超时
Timer? _inactivityTimer;
// 当前处理的Agent
Agent? _currentAgent;
// 本地消息历史备份
final List<Map<String, String>> _localMessageHistory = [];
// 获取消息历史 - 优先从ChatController获取,否则使用本地存储
List<Map<String, String>> get messageHistory {
if (Get.isRegistered<ChatController>()) {
final chatController = Get.find<ChatController>();
if (chatController.agent?.id == 'personal_assistant') {
return chatController.messages.map((m) => {
'role': m.role,
'content': m.content,
}).toList();
}
}
if (_localMessageHistory.isEmpty) {
_loadChatHistory();
}
return _localMessageHistory;
}
// 语音识别是否活跃
bool get isListening => _recognitionSubscription != null;
// TTS是否正在播放
bool get isTtsSpeaking => _ttsService.isActuallyPlaying();
@override
void onClose() {
_cleanupResources();
super.onClose();
}
/// 清理所有资源
void _cleanupResources() {
_recognitionSubscription?.cancel();
_aiResponseSubscription?.cancel();
if (_ttsService.isActuallyPlaying()) {
_ttsService.stop();
}
if (isListening) {
_asrService.stopRecognition();
}
_inactivityTimer?.cancel();
}
/// 开始代理交互
Future<void> startAgentInteraction(String agentId) async {
// 防止重复处理
if (isProcessing.value) {
Logger.warning('已经在处理交互,忽略此次请求');
return;
}
// 先说"我在听"
_ttsService.speak("我在!");
isProcessing.value = true;
try {
// 获取Agent
_currentAgent = AgentProvider.getAgentById(agentId);
if (_currentAgent == null) {
Logger.error('无法获取Agent: $agentId');
isProcessing.value = false;
return;
}
// 加载该Agent的聊天历史
_loadChatHistory();
// 启动语音识别
await _startVoiceRecognition();
// 启动无活动定时器
_startInactivityTimer();
} catch (e) {
Logger.error('启动代理交互失败', e);
isProcessing.value = false;
}
}
/// 停止语音识别
Future<void> _stopVoiceRecognition() async {
if (!isListening) return;
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
_asrService.stopRecognition();
recognizedText.value = '';
}
/// 开始语音识别
Future<void> _startVoiceRecognition() async {
if (isListening) return;
try {
final success = await _asrService.startRecognition();
if (success) {
_recognitionSubscription = _asrService.recognitionStream.listen(
(event) {
switch (event.type) {
case RecognitionEventType.recognizing:
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.isEmpty) return;
recognizedText.value = event.text;
Logger.info('处理语音识别最终结果:${event.text}');
_processRecognizedText(event.text);
recognizedText.value = '';
break;
case RecognitionEventType.error:
Logger.info('语音识别错误: ${event.error}');
stopInteraction();
break;
default:
break;
}
},
onError: (error) {
Logger.info('语音识别错误:$error');
stopInteraction();
},
);
} else {
Logger.error('启动语音识别失败');
}
} catch (e) {
Logger.error('启动语音识别失败', e);
isProcessing.value = false;
}
}
/// 中断当前响应
void _interruptCurrentResponse(String reason) {
Logger.info(reason);
if (_ttsService.isActuallyPlaying()) {
_ttsService.stop();
}
if (_aiResponseSubscription != null) {
_aiResponseSubscription!.cancel();
_aiResponseSubscription = null;
Logger.info('已中断AI响应流和TTS播放');
}
}
/// 处理识别到的文本
Future<void> _processRecognizedText(String text) async {
if (text.trim().isEmpty || _currentAgent == null) return;
try {
// 中断任何正在进行的响应
_interruptCurrentResponse("处理新的用户输入,中断当前响应");
// 添加用户消息到历史记录
_addMessageToHistory('user', text);
String fullResponse = '';
// 获取AI响应流
final responseStream = _aiService.sendMessageStream(
messages: messageHistory,
systemPrompt: _currentAgent!.systemPrompt,
);
// 订阅响应流
_aiResponseSubscription = responseStream.listen(
(chunk) {
fullResponse += chunk;
_ttsService.speak(chunk);
_resetInactivityTimer();
},
onDone: () {
_addMessageToHistory('assistant', fullResponse);
_aiResponseSubscription = null;
},
onError: (error) {
Logger.error('AI响应错误', error);
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speak(errorMessage);
_addMessageToHistory('assistant', errorMessage);
_aiResponseSubscription = null;
},
);
} catch (e) {
Logger.error('处理识别文本失败', e);
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speak(errorMessage);
_addMessageToHistory('assistant', errorMessage);
}
}
/// 重新启动语音识别
Future<void> _restartVoiceRecognition() async {
await _stopVoiceRecognition();
await Future.delayed(Duration(milliseconds: 500));
await _startVoiceRecognition();
}
/// 添加消息到历史记录
void _addMessageToHistory(String role, String content) {
// 添加到本地历史记录
_localMessageHistory.add({
'role': role,
'content': content,
});
// 保持本地历史记录不超过10条消息
if (_localMessageHistory.length > 10) {
_localMessageHistory.removeAt(0);
}
// 同步消息到ChatController或保存到本地存储
if (Get.isRegistered<ChatController>()) {
final chatController = Get.find<ChatController>();
if (chatController.agent?.id == 'personal_assistant') {
chatController.addMessage(Message(
role: role,
content: content,
timestamp: DateTime.now(),
));
}
} else {
_saveChatHistory();
}
}
// 加载聊天历史
void _loadChatHistory() {
if (_currentAgent == null) return;
try {
final messages = _historyService.loadHistory(_currentAgent!.id);
_localMessageHistory.clear();
for (final message in messages) {
_localMessageHistory.add({
'role': message.role,
'content': message.content,
});
}
} catch (e) {
Logger.error('加载聊天历史记录失败', e);
}
}
// 保存聊天历史
void _saveChatHistory() {
if (_currentAgent == null) return;
try {
final messages = _localMessageHistory.map((m) => Message(
role: m['role']!,
content: m['content']!,
timestamp: DateTime.now(),
)).toList();
_historyService.saveHistory(_currentAgent!.id, messages);
} catch (e) {
Logger.error('保存聊天历史记录失败', e);
}
}
/// 启动无活动定时器
void _startInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = Timer(Duration(seconds: 30), () {
if (!_ttsService.isActuallyPlaying() && isProcessing.value) {
Logger.info('检测到30秒内无活动,停止语音识别');
stopInteraction();
}
});
}
/// 重置无活动定时器
void _resetInactivityTimer() {
if (_inactivityTimer != null) {
_inactivityTimer!.cancel();
_startInactivityTimer();
}
}
/// 响应新的媒体事件(双击媒体按钮)
Future<void> respondToNewMediaEvent() async {
_ttsService.speak("我在!");
if (!isListening) {
await _restartVoiceRecognition();
}
_resetInactivityTimer();
}
/// 停止当前交互
Future<void> stopInteraction() async {
try {
_saveChatHistory();
_cleanupResources();
isProcessing.value = false;
_currentAgent = null;
Logger.info('已停止后台代理交互');
} catch (e) {
Logger.error('停止交互失败', e);
}
}
}

142
lib/data/services/flutter_asr_service.dart

@ -1,142 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
// 移除 speech_to_text 依赖
// import 'package:speech_to_text/speech_to_text.dart';
// import 'package:speech_to_text/speech_recognition_result.dart';
import '../../core/utils/logger.dart';
/// ASR状态枚举
enum AsrState {
notInitialized, // 未初始化
initialized, // 已初始化
listening, // 正在监听
notListening, // 未监听
error // 错误状态
}
/// 监听模式枚举(与 speech_to_text 兼容)
enum ListenMode {
deviceDefault,
dictation,
search,
confirmation,
}
/// 本地名称类(与 speech_to_text 兼容)
class LocaleName {
final String name;
final String localeId;
LocaleName(this.name, this.localeId);
}
/// Flutter ASR服务,负责本地语音转文本功能
/// 注意:此实现为空实现,不再依赖 speech_to_text 插件
class FlutterAsrService extends GetxService {
// 当前ASR状态
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value;
// 可用的语音识别语言
final RxList<LocaleName> _locales = <LocaleName>[].obs;
List<LocaleName> get locales => _locales;
// 当前选择的语言
final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value;
// 识别结果
final RxString _lastWords = ''.obs;
String get lastWords => _lastWords.value;
// 识别准确度
final RxDouble _confidence = 0.0.obs;
double get confidence => _confidence.value;
// 是否可用
final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value;
// 是否正在监听
bool _isListening = false;
bool get isListening => _isListening;
// 识别结果流
final _recognitionResultsController = StreamController<String>.broadcast();
Stream<String> get recognitionResults => _recognitionResultsController.stream;
@override
void onInit() {
super.onInit();
_initSpeech();
}
/// 初始化语音识别
Future<void> _initSpeech() async {
try {
// 空实现,不再使用 speech_to_text
_isAvailable.value = false;
_asrState.value = AsrState.error;
// 添加一个默认语言
_locales.add(LocaleName('中文 (简体)', 'zh-CN'));
_currentLocale.value = 'zh-CN';
Logger.info('FlutterAsrService 已初始化(空实现)');
} catch (e) {
_asrState.value = AsrState.error;
_isAvailable.value = false;
Logger.error('语音识别初始化失败: $e');
}
}
/// 开始监听
///
/// [listenFor] 监听时长,默认30秒
/// [pauseFor] 暂停时长,默认3秒,超过此时长无语音输入会自动停止
/// [partialResults] 是否返回部分结果,默认true
/// [onDevice] 是否使用设备本地识别,默认false
/// [listenMode] 监听模式,默认设备默认模式
Future<bool> startListening({
Duration listenFor = const Duration(seconds: 30),
Duration pauseFor = const Duration(seconds: 3),
bool partialResults = true,
bool onDevice = false,
ListenMode listenMode = ListenMode.deviceDefault,
}) async {
// 空实现,不再使用 speech_to_text
Logger.error('FlutterAsrService 为空实现,不支持语音识别');
return false;
}
/// 停止监听
Future<void> stopListening() async {
// 空实现,不再使用 speech_to_text
_isListening = false;
_asrState.value = AsrState.notListening;
}
/// 取消监听
Future<void> cancelListening() async {
// 空实现,不再使用 speech_to_text
_isListening = false;
_lastWords.value = '';
_confidence.value = 0.0;
_asrState.value = AsrState.notListening;
}
/// 设置当前语言
void setLocale(String localeId) {
if (_currentLocale.value != localeId) {
_currentLocale.value = localeId;
Logger.info('设置语音识别语言: $localeId');
}
}
@override
void onClose() {
_recognitionResultsController.close();
super.onClose();
}
}

367
lib/data/services/flutter_tts_service.dart

@ -1,364 +1,3 @@
import 'dart:io';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
/// Flutter TTS服务,负责本地文本转语音功能
class FlutterTtsService extends GetxService {
late FlutterTts _flutterTts;
// 当前TTS状态
TtsState _ttsState = TtsState.stopped;
TtsState get ttsState => _ttsState;
// 可用的语音
List<Map<String, String>> _voices = [];
List<Map<String, String>> get voices => _voices;
// 当前选择的语音
String _currentVoice = '';
String get currentVoice => _currentVoice;
set currentVoice(String voice) => _currentVoice = voice;
// 语速 (0.1-2.0)
double _rate = 0.5;
double get rate => _rate;
set rate(double value) => _rate = value;
// 音量 (0.0-1.0)
double _volume = 1.0;
double get volume => _volume;
set volume(double value) => _volume = value;
// 音调 (0.5-2.0)
double _pitch = 1.0;
double get pitch => _pitch;
set pitch(double value) => _pitch = value;
// 文本队列 - 仅包含待播放的文本
List<String> _textQueue = [];
List<String> get textQueue => _textQueue;
// 当前正在播放的文本
String _currentText = '';
String get currentText => _currentText;
// 是否正在处理队列
bool _isProcessingQueue = false;
@override
void onInit() {
super.onInit();
_initTts();
}
/// 初始化TTS引擎
Future<void> _initTts() async {
_flutterTts = FlutterTts();
// 设置完成回调
_flutterTts.setCompletionHandler(() {
Logger.info('TTS播放完成: "$_currentText"');
// 清空当前播放文本
_currentText = '';
// 处理队列中的下一条文本
_processNextInQueue();
// 只有当队列为空且不再处理队列时才设置为stopped状态
if (_textQueue.isEmpty && !_isProcessingQueue) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列播放完成,设置为stopped状态');
}
});
// 设置错误回调
_flutterTts.setErrorHandler((error) {
Logger.error('Flutter TTS错误: $error');
// 清空当前播放文本
_currentText = '';
// 尝试播放下一条文本
_processNextInQueue();
// 只有当队列为空且不再处理队列时才设置为stopped状态
if (_textQueue.isEmpty && !_isProcessingQueue) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列播放出错且为空,设置为stopped状态');
}
});
// 设置开始回调
_flutterTts.setStartHandler(() {
_ttsState = TtsState.playing;
});
// 设置取消回调
_flutterTts.setCancelHandler(() {
// 清空当前播放文本
_currentText = '';
_isProcessingQueue = false;
_ttsState = TtsState.stopped;
Logger.info('TTS播放被取消,设置为stopped状态');
});
// 设置暂停回调
_flutterTts.setPauseHandler(() {
_ttsState = TtsState.paused;
});
// 设置继续回调
_flutterTts.setContinueHandler(() {
_ttsState = TtsState.playing;
});
// 加载可用语音
await _loadVoices();
Logger.info('Flutter TTS服务已初始化');
}
/// 加载可用的语音
Future<void> _loadVoices() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final voices = await _flutterTts.getVoices;
if (voices != null) {
_voices = [];
for (var voice in voices) {
if (voice is Map) {
final Map<String, String> voiceMap = {};
voice.forEach((key, value) {
if (key is String && value is String) {
voiceMap[key] = value;
} else if (key is String) {
voiceMap[key] = value.toString();
}
});
if (voiceMap.containsKey('name')) {
_voices.add(voiceMap);
}
}
}
// 如果有可用语音,设置第一个为默认
if (_voices.isNotEmpty) {
_currentVoice = _voices[0]['name'] ?? '';
await _flutterTts.setVoice({"name": _currentVoice});
}
}
}
} catch (e) {
Logger.error('加载TTS语音失败', e);
}
}
/// 设置语音
Future<void> setVoice(String voiceName) async {
try {
_currentVoice = voiceName;
await _flutterTts.setVoice({"name": voiceName});
} catch (e) {
Logger.error('设置TTS语音失败', e);
}
}
/// 设置语速
Future<void> setRate(double rate) async {
try {
_rate = rate;
await _flutterTts.setSpeechRate(rate);
} catch (e) {
Logger.error('设置TTS语速失败', e);
}
}
/// 设置音量
Future<void> setVolume(double volume) async {
try {
_volume = volume;
await _flutterTts.setVolume(volume);
} catch (e) {
Logger.error('设置TTS音量失败', e);
}
}
/// 设置音调
Future<void> setPitch(double pitch) async {
try {
_pitch = pitch;
await _flutterTts.setPitch(pitch);
} catch (e) {
Logger.error('设置TTS音调失败', e);
}
}
/// 设置语言
Future<void> setLanguage(String language) async {
try {
await _flutterTts.setLanguage(language);
} catch (e) {
Logger.error('设置TTS语言失败', e);
}
}
/// 添加文本到队列并开始处理队列(如果尚未开始)
Future<void> speak(String text) async {
if (text.isEmpty) return;
// 添加到队列
_addToQueue(text);
// 开始处理队列(如果尚未开始)
_startQueueProcessingIfNeeded();
}
/// 添加多条文本到队列并开始处理队列(如果尚未开始)
Future<void> speakTexts(List<String> texts) async {
if (texts.isEmpty) return;
// 添加所有非空文本到队列
int addedCount = 0;
for (final text in texts) {
if (text.isNotEmpty) {
_textQueue.add(text);
addedCount++;
}
}
if (addedCount > 0) {
Logger.info('已添加$addedCount条文本到TTS队列');
// 开始处理队列(如果尚未开始)
_startQueueProcessingIfNeeded();
}
}
/// 添加文本到队列
void _addToQueue(String text) {
if (text.isEmpty) return;
_textQueue.add(text);
Logger.info('已添加文本到TTS队列: "${text.length > 20 ? "${text.substring(0, 20)}..." : text}"');
}
/// 开始处理队列(如果尚未开始)
void _startQueueProcessingIfNeeded() {
// 如果队列为空,不需要处理
if (_textQueue.isEmpty) return;
// 如果已经在处理队列或正在播放,不需要重新开始
if (_isProcessingQueue || _ttsState == TtsState.playing) return;
// 开始处理队列
_isProcessingQueue = true;
_processNextInQueue();
}
/// 处理队列中的下一条文本
void _processNextInQueue() {
// 如果队列为空,结束处理但不立即设置stopped状态
if (_textQueue.isEmpty) {
_currentText = '';
_isProcessingQueue = false;
// 只有当当前没有文本在播放时才设置为stopped状态
if (_currentText.isEmpty) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列为空,设置为stopped状态');
}
return;
}
// 获取队列中的第一条文本并播放
_speakNextText();
}
/// 播放队列中的下一条文本
Future<void> _speakNextText() async {
if (_textQueue.isEmpty) {
_isProcessingQueue = false;
// 只有当当前没有文本在播放时才设置为stopped状态
if (_currentText.isEmpty) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列播放完成,设置为stopped状态');
}
return;
}
// 获取队列中的第一条文本
final text = _textQueue.removeAt(0);
_currentText = text;
try {
// 设置TTS参数
await _flutterTts.setSpeechRate(_rate);
await _flutterTts.setVolume(_volume);
await _flutterTts.setPitch(_pitch);
if (_currentVoice.isNotEmpty) {
await _flutterTts.setVoice({"name": _currentVoice});
}
// 播放文本
await _flutterTts.speak(text);
Logger.info('正在播放文本,队列中还有${_textQueue.length}条待播放');
} catch (e) {
Logger.error('TTS播放文本失败', e);
// 清空当前文本
_currentText = '';
// 尝试播放下一个
_processNextInQueue();
}
}
/// 停止播放并清空队列
Future<void> stop() async {
try {
await _flutterTts.stop();
// 重置队列处理状态
_textQueue.clear();
_currentText = '';
_isProcessingQueue = false;
_ttsState = TtsState.stopped;
Logger.info('已停止TTS播放并清空队列');
} catch (e) {
Logger.error('停止TTS播放失败', e);
}
}
/// 暂停播放
Future<void> pause() async {
try {
await _flutterTts.pause();
_ttsState = TtsState.paused;
} catch (e) {
Logger.error('暂停TTS播放失败', e);
}
}
/// 清空队列并停止播放
Future<void> clearQueue() async {
_textQueue.clear();
await stop();
Logger.info('已清空TTS队列');
}
@override
void onClose() {
_flutterTts.stop();
super.onClose();
}
}
/// TTS状态枚举
enum TtsState {
playing,
stopped,
paused,
}
// 重新导出FlutterTtsService类
// 这个文件作为兼容层,将speech_impl/flutter_tts_service.dart中的服务导出
export 'speech_impl/flutter_tts_service.dart';

352
lib/data/services/my_audio_handler.dart

@ -1,352 +0,0 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:get/get.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'background_agent_service.dart';
import '../../data/providers/agent_provider.dart';
import '../../core/utils/logger.dart';
/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机按钮)
class MyAudioHandler extends BaseAudioHandler {
bool _isDisposed = false;
// 添加一个标志,表示是否正在处理交互
bool _isHandlingInteraction = false;
// 添加一个计时器,用于定期更新播放状态
Timer? _statusUpdateTimer;
// 添加一个变量,记录最后一次媒体按钮事件的时间
DateTime? _lastMediaButtonEventTime;
MyAudioHandler() {
_initializeHandler();
_startStatusUpdateTimer();
Logger.info('MyAudioHandler 已创建,准备接收媒体按钮事件');
}
// 获取BackgroundAgentService实例
BackgroundAgentService? _getBackgroundAgent() {
try {
if (Get.isRegistered<BackgroundAgentService>()) {
return Get.find<BackgroundAgentService>();
} else {
return Get.put(BackgroundAgentService(), permanent: true);
}
} catch (e) {
Logger.error('获取 BackgroundAgent 失败', e);
return null;
}
}
void _initializeHandler() {
try {
// 设置初始播放状态
playbackState.add(PlaybackState(
controls: [
MediaControl.play,
MediaControl.pause,
MediaControl.stop,
MediaControl.skipToPrevious,
MediaControl.skipToNext,
],
systemActions: {
MediaAction.play,
MediaAction.pause,
MediaAction.stop,
MediaAction.skipToPrevious,
MediaAction.skipToNext,
MediaAction.seek,
},
androidCompactActionIndices: const [0, 1, 2],
processingState: AudioProcessingState.idle,
playing: false,
));
// 设置媒体元数据
final item = MediaItem(
id: 'deep_voice_audio',
title: 'Deep Voice Audio',
album: 'Deep Voice',
artist: 'Deep Voice',
genre: 'Speech',
duration: const Duration(seconds: 1),
artUri: null, // 移除图片URI
playable: true,
displayTitle: 'Deep Voice Audio',
displaySubtitle: 'Voice Assistant',
displayDescription: 'TTS Audio',
extras: const {'source': 'deep_voice'},
);
mediaItem.add(item);
Logger.info('AudioHandler 媒体项已设置');
// 立即更新播放状态,确保通知显示
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
} catch (e) {
Logger.error('初始化 AudioHandler 失败', e);
}
}
// 启动定期更新播放状态的计时器
void _startStatusUpdateTimer() {
_statusUpdateTimer?.cancel();
_statusUpdateTimer = Timer.periodic(Duration(minutes: 5), (timer) {
try {
// 定期更新播放状态,确保服务保持活跃
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
Logger.info('定期更新播放状态成功');
} catch (e) {
Logger.error('定期更新播放状态失败', e);
}
});
}
// 更新播放状态
void _updatePlaybackState({
required bool playing,
required AudioProcessingState processingState,
}) {
if (_isDisposed) return;
try {
playbackState.add(playbackState.value.copyWith(
playing: playing,
processingState: processingState,
updatePosition: Duration.zero,
));
} catch (e) {
Logger.error('更新播放状态失败', e);
}
}
Future<void> _handleInteraction() async {
// 防止重复处理
if (_isHandlingInteraction) {
Logger.info('已经在处理交互,忽略此次请求');
return;
}
_isHandlingInteraction = true;
try {
// 更新播放状态,确保通知显示
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
// 直接获取 personal_assistant agent
final agent = AgentProvider.getAgentById('personal_assistant');
if (agent == null) {
Logger.error('无法获取personal_assistant agent,无法激活语音助手');
return;
}
// 检查app是否在前台
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();
Logger.info('已停止后台代理服务,切换到前台模式');
}
// 导航到聊天页面,并标记需要在进入时播放语音应答
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentVoice': agent.voice,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt,
'welcomeMessage': agent.welcomeMessage,
'playVoiceOnEnter': true,
});
} else if (backgroundAgent != null) {
// 应用在后台,使用 BackgroundAgent 处理交互
if (backgroundAgent.isProcessing.value) {
Logger.info('检测到媒体按钮再次按下,中断当前AI回复');
await backgroundAgent.respondToNewMediaEvent();
} else {
// 首次启动后台服务
Logger.info('首次启动后台服务');
await backgroundAgent.startAgentInteraction(agent.id);
}
} else {
Logger.error('无法获取 BackgroundAgent,无法处理交互');
}
} catch (e) {
Logger.error('处理媒体按钮交互失败', e);
} finally {
_isHandlingInteraction = false;
}
}
@override
Future<void> play() async {
Logger.info('收到播放命令');
try {
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
await _handleInteraction();
} catch (e) {
Logger.error('处理播放命令失败', e);
}
}
@override
Future<void> pause() async {
Logger.info('收到暂停命令');
try {
_updatePlaybackState(
playing: false,
processingState: AudioProcessingState.ready,
);
await _handleInteraction();
} catch (e) {
Logger.error('处理暂停命令失败', e);
}
}
@override
Future<void> stop() async {
Logger.info('停止 AudioHandler');
_isDisposed = true;
try {
_statusUpdateTimer?.cancel();
// 如果有正在运行的后台服务,停止它
final backgroundAgent = _getBackgroundAgent();
if (backgroundAgent != null && backgroundAgent.isProcessing.value) {
await backgroundAgent.stopInteraction();
}
await super.stop();
_updatePlaybackState(
playing: false,
processingState: AudioProcessingState.idle,
);
} catch (e) {
Logger.error('停止 AudioHandler 时发生错误', e);
}
}
@override
Future<void> skipToPrevious() async {
Logger.info('收到上一曲命令');
try {
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
await _handleInteraction();
} catch (e) {
Logger.error('处理上一曲命令失败', e);
}
}
@override
Future<void> skipToNext() async {
Logger.info('收到下一曲命令');
try {
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
await _handleInteraction();
} catch (e) {
Logger.error('处理下一曲命令失败', e);
}
}
@override
Future<void> seek(Duration position) async {
Logger.info('收到跳转命令: $position');
}
@override
Future<dynamic> customAction(String name,
[Map<String, dynamic>? extras]) async {
Logger.info('收到自定义命令: $name');
try {
if (name == 'media_button') {
Logger.info('收到媒体按钮事件');
// 检查是否是重复触发(防止短时间内多次触发)
final now = DateTime.now();
if (_lastMediaButtonEventTime != null) {
final difference = now.difference(_lastMediaButtonEventTime!);
if (difference.inMilliseconds < 500) {
Logger.info('媒体按钮事件触发过于频繁,忽略此次事件');
return;
}
}
// 更新最后一次事件时间
_lastMediaButtonEventTime = now;
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
await _handleInteraction();
} else if (name == 'ping') {
// 用于检查服务是否响应的测试命令
Logger.info('收到 ping 命令,服务正常运行');
return 'pong';
}
} catch (e) {
Logger.error('处理自定义命令失败', e);
}
}
@override
Future<void> onTaskRemoved() async {
Logger.info('服务被系统移除');
try {
// 如果有正在运行的后台服务,停止它
final backgroundAgent = _getBackgroundAgent();
if (backgroundAgent != null && backgroundAgent.isProcessing.value) {
await backgroundAgent.stopInteraction();
}
await stop();
} catch (e) {
Logger.error('处理服务移除失败', e);
}
}
@override
Future<void> onNotificationDeleted() async {
Logger.info('通知被用户移除');
try {
// 如果有正在运行的后台服务,停止它
final backgroundAgent = _getBackgroundAgent();
if (backgroundAgent != null && backgroundAgent.isProcessing.value) {
await backgroundAgent.stopInteraction();
}
await stop();
} catch (e) {
Logger.error('处理通知移除失败', e);
}
}
}

224
lib/data/services/notification_service.dart

@ -1,224 +0,0 @@
import 'dart:io';
import 'package:get/get.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import '../providers/agent_provider.dart';
import 'package:flutter/foundation.dart';
class NotificationService extends GetxService {
static NotificationService get to => Get.find();
FlutterLocalNotificationsPlugin? _flutterLocalNotificationsPlugin;
bool _notificationActive = false;
bool _isRequestingPermission = false;
String get _defaultAgentName {
final agent = AgentProvider.getAgentById('personal_assistant');
return agent?.name ?? 'Deep Voice';
}
Future<void> init() async {
try {
if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) {
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
// 使用应用图标作为通知图标
const initializationSettingsAndroid = AndroidInitializationSettings('mipmap/ic_launcher');
final initializationSettingsDarwin = DarwinInitializationSettings(
requestSoundPermission: false,
requestBadgePermission: false,
requestAlertPermission: false,
);
final initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin,
);
await _flutterLocalNotificationsPlugin?.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (details) {
if (Get.currentRoute != '/chat') {
Get.toNamed('/chat');
}
},
);
}
// 创建通知渠道 - 使用高重要性确保通知在后台可见
if (Platform.isAndroid) {
await _flutterLocalNotificationsPlugin?.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(
const AndroidNotificationChannel(
'foreground_service',
'Foreground Service',
description: 'Used for the foreground service notification',
importance: Importance.high, // 提高重要性
playSound: false,
enableVibration: false,
showBadge: true,
),
);
}
// 请求权限,但避免重复请求
await _requestPermissionIfNeeded();
// 在初始化完成后显示前台服务通知
if (Platform.isAndroid) {
await showForegroundNotification();
}
// Add macOS-specific code for status bar icon
if (Platform.isMacOS) {
await showMacOSStatusBarIcon();
}
} catch (e) {
print('通知服务初始化失败: $e');
}
}
Future<void> showForegroundNotification() async {
if (_notificationActive) return;
try {
final androidDetails = AndroidNotificationDetails(
'foreground_service',
'Foreground Service',
channelDescription: 'Used for the foreground service notification',
importance: Importance.high, // 提高重要性
priority: Priority.high, // 提高优先级
ongoing: true,
autoCancel: false,
showWhen: false,
enableVibration: false,
playSound: false,
icon: 'mipmap/ic_launcher', // 使用应用图标
visibility: NotificationVisibility.public,
category: AndroidNotificationCategory.service,
actions: [
const AndroidNotificationAction(
'chat',
'开始对话',
showsUserInterface: true,
),
],
);
const iosDetails = DarwinNotificationDetails(
presentAlert: false,
presentBadge: false,
presentSound: false,
interruptionLevel: InterruptionLevel.passive,
);
final notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _flutterLocalNotificationsPlugin?.show(
0,
_defaultAgentName,
'点击开始对话',
notificationDetails,
);
_notificationActive = true;
} catch (e) {
print('显示前台服务通知失败: $e');
}
}
Future<void> cancelNotification() async {
if (!_notificationActive) return;
try {
await _flutterLocalNotificationsPlugin?.cancel(0);
_notificationActive = false;
} catch (e) {
print('取消通知失败: $e');
}
}
Future<void> _requestPermissionIfNeeded() async {
// 避免重复请求权限
if (_isRequestingPermission) {
print('已有权限请求正在进行中,跳过此次请求');
return;
}
_isRequestingPermission = true;
try {
if (Platform.isMacOS) {
// On macOS, we don't need to request notification permissions through permission_handler
// Instead, we'll use the flutter_local_notifications plugin directly
final bool? granted = await _flutterLocalNotificationsPlugin
?.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
if (granted != true) {
debugPrint('MacOS Notifications: Permission request was denied');
}
} else if (Platform.isAndroid || Platform.isIOS) {
// 检查权限状态,避免不必要的请求
final status = await Permission.notification.status;
if (status != PermissionStatus.granted) {
await Permission.notification.request();
}
}
} catch (e) {
print('请求通知权限失败: $e');
} finally {
_isRequestingPermission = false;
}
}
Future<void> showNotification({
required String title,
required String body,
}) async {
if (_flutterLocalNotificationsPlugin == null) return;
try {
const androidDetails = AndroidNotificationDetails(
'chat_channel',
'聊天消息',
channelDescription: '接收新的聊天消息',
importance: Importance.high,
priority: Priority.high,
icon: 'mipmap/ic_launcher', // 使用应用图标
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _flutterLocalNotificationsPlugin?.show(
0,
title,
body,
notificationDetails,
);
} catch (e) {
print('显示通知失败: $e');
}
}
Future<void> showMacOSStatusBarIcon() async {
// Implementation using platform channel to communicate with native macOS code
}
}

108
lib/data/services/speech_factory.dart

@ -0,0 +1,108 @@
import 'package:get/get.dart';
import 'asr_service.dart';
import 'tts_service.dart';
import 'speech_impl/azure_asr_service.dart';
import 'speech_impl/azure_tts_service.dart';
import 'speech_impl/flutter_asr_service.dart';
import 'speech_impl/flutter_tts_service.dart';
import '../../core/utils/logger.dart';
/// 语音服务类型
enum SpeechServiceType {
/// Azure 语音服务
azure,
/// Flutter 本地语音服务
flutter,
}
/// 语音服务工厂类
///
/// 用于根据配置信息获取不同的TTS和ASR实现
class SpeechFactory extends GetxService {
static SpeechFactory get to => Get.find<SpeechFactory>();
/// 当前使用的语音服务类型
SpeechServiceType _currentServiceType = SpeechServiceType.azure;
/// 获取当前使用的语音服务类型
SpeechServiceType get currentServiceType => _currentServiceType;
/// 设置语音服务类型
void setServiceType(SpeechServiceType type) {
if (_currentServiceType != type) {
_currentServiceType = type;
Logger.info('语音服务类型已切换为: ${type.toString().split('.').last}');
// 重新注册服务
_registerServices();
}
}
/// 注册语音服务
void _registerServices() {
// 注销现有服务
if (Get.isRegistered<AsrService>()) {
Get.delete<AsrService>(force: true);
}
if (Get.isRegistered<TtsService>()) {
Get.delete<TtsService>(force: true);
}
// 根据类型注册新服务
switch (_currentServiceType) {
case SpeechServiceType.azure:
Get.lazyPut<AsrService>(() => AzureAsrService());
Get.lazyPut<TtsService>(() => AzureTtsService());
break;
case SpeechServiceType.flutter:
Get.lazyPut<AsrService>(() => FlutterAsrService());
Get.lazyPut<TtsService>(() => FlutterTtsService());
break;
}
}
/// 初始化语音服务
Future<void> initialize({SpeechServiceType? initialType}) async {
try {
// 如果指定了初始类型,则设置
if (initialType != null) {
_currentServiceType = initialType;
}
// 注册服务
_registerServices();
// // 初始化服务
// final asrService = Get.find<AsrService>();
// final ttsService = Get.find<TtsService>();
// await asrService.initialize();
// await ttsService.initialize();
Logger.info('语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) {
Logger.error('语音服务初始化失败: $e');
rethrow;
}
}
/// 获取ASR服务
AsrService getAsrService() {
if (!Get.isRegistered<AsrService>()) {
_registerServices();
}
return Get.find<AsrService>();
}
/// 获取TTS服务
TtsService getTtsService() {
if (!Get.isRegistered<TtsService>()) {
_registerServices();
}
return Get.find<TtsService>();
}
}

123
lib/data/services/azure_asr_service.dart → lib/data/services/speech_impl/azure_asr_service.dart

@ -3,10 +3,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../../core/utils/logger.dart';
import 'package:get/get.dart';
/// 微软语音识别服务
import '../asr_service.dart';
/// Azure 语音识别服务
///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class AzureAsrService {
class AzureAsrService extends GetxService implements AsrService {
static final AzureAsrService to = Get.put(AzureAsrService());
static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_asr');
static const EventChannel _eventChannel = EventChannel('com.deep_voice.azure_asr_events');
@ -15,8 +17,8 @@ class AzureAsrService {
late final String _subscriptionKey;
late final String _serviceRegion;
List<String> _supportedLanguages = ['zh-CN', 'en-US'];
final List<String> _supportedLanguages = ['zh-CN'];
@override
List<String> get supportedLanguages => _supportedLanguages;
// 连续识别相关
@ -28,10 +30,6 @@ class AzureAsrService {
bool _isEventChannelReady = false;
Completer<void>? _eventChannelReadyCompleter;
// 公开的事件流
Stream<RecognitionEvent>? _recognitionStream;
Stream<RecognitionEvent>? get recognitionStream => _recognitionStream;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
@ -92,16 +90,14 @@ class AzureAsrService {
}
}
/// 初始化微软语音识别 SDK
///
/// 返回 true 表示初始化成功,否则抛出 PlatformException
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages = supportedLanguages;
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 检查是否需要重新初始化
@ -125,11 +121,8 @@ class AzureAsrService {
}
}
/// 执行一次性语音识别
///
/// 返回识别的文本和检测到的语言,如果识别失败则抛出 PlatformException
Future<RecognitionResult> recognizeSpeech() async {
@override
Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) {
await initialize();
}
@ -142,23 +135,25 @@ class AzureAsrService {
final String detectedLanguage = result['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
return RecognitionResult(text: text, detectedLanguage: detectedLanguage);
return RecognitionEvent.finalResult(
text: text,
detectedLanguage: detectedLanguage,
);
} else if (result is String) {
// 兼容旧版本
_latestRecognizedText = result;
return RecognitionResult(text: result);
return RecognitionEvent.finalResult(text: result);
}
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
rethrow;
return RecognitionEvent.error(e.toString());
}
}
/// 开始连续语音识别
///
/// 返回一个包含识别事件的流,如果开始失败则抛出 PlatformException
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
@ -193,9 +188,7 @@ class AzureAsrService {
}
}
/// 停止连续语音识别
///
/// 返回 true 表示停止成功,否则抛出 PlatformException
@override
Future<bool> stopContinuousRecognition() async {
if (!_isInitialized || !_isContinuousRecognitionActive) {
return true;
@ -212,7 +205,7 @@ class AzureAsrService {
}
}
/// 检查连续识别是否活跃
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
@ -300,13 +293,17 @@ class AzureAsrService {
_isContinuousRecognitionActive = false;
}
/// 释放资源
@override
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 取消事件订阅
await _eventSubscription?.cancel();
_eventSubscription = null;
// 通知原生端释放资源
await _channel.invokeMethod('dispose');
_isInitialized = false;
@ -318,72 +315,4 @@ class AzureAsrService {
_isInitialized = false;
}
}
}
/// 识别事件类型
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 识别错误
error,
}
/// 识别事件
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
/// 检测到的语言(仅在启用自动语言检测时有效)
final String detectedLanguage;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
RecognitionEvent({
required this.type,
this.text = '',
this.detectedLanguage = '',
this.error = '',
});
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}';
}
}
/// 识别结果
class RecognitionResult {
/// 识别文本
final String text;
/// 检测到的语言(仅在启用自动语言检测时有效)
final String detectedLanguage;
RecognitionResult({
required this.text,
this.detectedLanguage = '',
});
@override
String toString() {
return 'RecognitionResult{text: $text, detectedLanguage: $detectedLanguage}';
}
}

570
lib/data/services/speech_impl/azure_tts_service.dart

@ -0,0 +1,570 @@
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';
import '../tts_service.dart';
/// 音频输出设备类型
enum AudioOutputType {
/// 扬声器
speaker,
/// 听筒
earpiece,
/// 自动选择(有耳机用耳机,没有则用听筒)
auto
}
/// 微软 Text-to-Speech 服务
///
/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService());
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;
AudioOutputType _currentAudioOutputType = AudioOutputType.auto;
// 支持的语音列表
final List<String> _supportedVoices = [
'zh-CN-XiaoxiaoNeural',
'zh-CN-YunxiNeural',
'zh-CN-YunjianNeural',
'en-US-JennyNeural',
'en-US-GuyNeural'
];
// 语音合成队列
final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false;
// 事件流控制器
final StreamController<TtsEvent> _eventController = StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
// 可观察状态
final isEnabled = true.obs;
final _isSpeaking = false.obs;
final audioOutputType = AudioOutputType.auto.obs;
// 流式文本缓冲区
String _streamBuffer = '';
@override
String get currentVoice => _currentVoice;
@override
bool get isSpeaking => _isSpeaking.value;
@override
List<String> get supportedVoices => _supportedVoices;
AudioOutputType get currentAudioOutputType => _currentAudioOutputType;
AzureTtsService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
if (_isInitialized) return true;
try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
Logger.error('未找到Azure语音服务配置');
return false;
}
// 始终优先使用中文作为默认语言
final String defaultLanguage = 'zh-CN';
// 更新支持的语音列表(如果提供)
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
// 如果支持中文,优先使用中文,否则使用提供的第一种语言
String language = supportedLanguages.contains(defaultLanguage)
? defaultLanguage
: supportedLanguages.first;
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': language,
});
_isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
// 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值
// 只需要确保 Dart 层的状态与原生层一致
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
return result;
} else {
// 使用默认语言初始化
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': 'zh-CN',
});
_isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
return result;
}
} catch (e) {
Logger.error('初始化失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('TTS初始化失败: $e'));
return false;
}
}
@override
Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize();
if (voiceName == _currentVoice) return true;
try {
final result = await _channel.invokeMethod('setVoice', {
'voiceName': voiceName,
});
if (result) _currentVoice = voiceName;
return result;
} catch (e) {
Logger.error('设置语音失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音失败: $e'));
return false;
}
}
/// 设置音频输出设备类型
Future<bool> setAudioOutputType(AudioOutputType outputType) async {
if (!_isInitialized) await initialize();
try {
final String outputTypeStr = outputType.toString().split('.').last;
final result = await _channel.invokeMethod('setAudioOutputType', {
'outputType': outputTypeStr,
});
if (result) {
_currentAudioOutputType = outputType;
audioOutputType.value = outputType;
}
return result;
} catch (e) {
Logger.error('设置音频输出设备失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.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');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e'));
return false;
}
}
@override
Future<bool> speakOnce(String text) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value || text.isEmpty) return false;
try {
_isSpeaking.value = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
final result = await _channel.invokeMethod('speakText', {'text': text});
_isSpeaking.value = false;
// 发送完成事件
_eventController.add(TtsEvent(type: TtsEventType.completed));
return result == "OK";
} catch (e) {
_isSpeaking.value = false;
Logger.error('语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
return false;
}
}
/// 播放SSML
Future<bool> speakSsmlOnce(String ssml) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value || ssml.isEmpty) return false;
try {
_isSpeaking.value = true;
// 发送开始事件
_eventController.add(TtsEvent(type: TtsEventType.started));
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
_isSpeaking.value = false;
// 发送完成事件
_eventController.add(TtsEvent(type: TtsEventType.completed));
return result == "OK";
} catch (e) {
_isSpeaking.value = false;
Logger.error('SSML语音合成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('SSML语音合成失败: $e'));
return false;
}
}
@override
Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
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;
} catch (e) {
Logger.error('处理流式文本失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem(
text: _streamBuffer,
rate: _currentRate,
pitch: _currentPitch,
volume: _currentVolume,
));
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成
while (_isProcessingQueue || _isSpeaking.value) {
await Future.delayed(const Duration(milliseconds: 100));
}
Logger.info('TTS队列播放完成');
return true;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
return false;
}
}
/// 处理语音合成队列
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,
);
}
// 使用speakOnce播放当前项
await speakOnce(item.text);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
} finally {
_isProcessingQueue = false;
}
}
@override
Future<bool> stop() async {
try {
_textQueue.clear();
if (_isSpeaking.value) {
final result = await _channel.invokeMethod('stopSpeaking');
_isSpeaking.value = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
return result;
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false;
}
}
/// 生成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<bool> toggleAudioOutputType() async {
AudioOutputType newType;
switch (_currentAudioOutputType) {
case AudioOutputType.speaker:
newType = AudioOutputType.earpiece;
break;
case AudioOutputType.earpiece:
newType = AudioOutputType.auto;
break;
case AudioOutputType.auto:
newType = AudioOutputType.speaker;
break;
}
return await setAudioOutputType(newType);
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
await _channel.invokeMethod('dispose');
// 关闭事件流控制器
await _eventController.close();
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
}
}
/// 处理AI返回的文本流,将其转换为语音
///
/// [textStream] 文本流
/// 返回一个Future,当流处理完成时完成
Future<bool> processAiTextStream(Stream<String> textStream) async {
if (!isEnabled.value) {
Logger.error('TTS服务未启用');
return false;
}
try {
await for (final text in textStream) {
if (text.isEmpty) continue;
await speakStream(text);
}
// 处理完流后,刷新缓冲区中的剩余文本
await flushStream();
return true;
} catch (e) {
Logger.error('处理文本流时出错: $e');
return false;
}
}
}
/// 语音合成项目
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,
});
}

439
lib/data/services/speech_impl/flutter_asr_service.dart

@ -0,0 +1,439 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_recognition_error.dart';
import '../../../core/utils/logger.dart';
import '../asr_service.dart';
/// ASR状态枚举
enum AsrState {
/// 未初始化
notInitialized,
/// 已初始化但未开始监听
initialized,
/// 正在监听
listening,
/// 正在处理识别结果
processing,
/// 识别完成
done,
/// 发生错误
error,
}
/// Flutter ASR服务,负责本地语音转文本功能
/// 使用speech_to_text插件实现语音识别功能
class FlutterAsrService extends GetxService implements AsrService {
static final FlutterAsrService to = Get.put(FlutterAsrService());
// speech_to_text 实例
final stt.SpeechToText _speech = stt.SpeechToText();
// 可用的语音识别语言
final List<String> _supportedLanguages = [];
@override
List<String> get supportedLanguages => _supportedLanguages;
// 当前选择的语言
final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value;
// 是否可用
final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value;
// 是否正在监听
bool _isListening = false;
// 当前ASR状态
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value;
// 连续识别模式的结果流控制器
StreamController<RecognitionEvent>? _continuousRecognitionController;
FlutterAsrService() {
_initSpeech();
}
/// 初始化语音识别
Future<void> _initSpeech() async {
try {
_asrState.value = AsrState.notInitialized;
// 初始化语音识别,使用较短的最终超时时间,提高响应速度
_isAvailable.value = await _speech.initialize(
onStatus: _onSpeechStatus,
onError: _onSpeechError,
debugLogging: false,
finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回
);
if (_isAvailable.value) {
// 获取支持的语言列表
final systemLocales = await _speech.locales();
// 清空并重新填充支持的语言列表
_supportedLanguages.clear();
for (var locale in systemLocales) {
if (!_supportedLanguages.contains(locale.localeId)) {
_supportedLanguages.add(locale.localeId);
}
}
// 设置默认语言(优先使用系统语言或中文)
var systemLocale = await _speech.systemLocale();
if (systemLocale != null) {
_currentLocale.value = systemLocale.localeId;
} else if (_supportedLanguages.contains('zh-CN')) {
_currentLocale.value = 'zh-CN';
} else if (_supportedLanguages.isNotEmpty) {
_currentLocale.value = _supportedLanguages.first;
}
_asrState.value = AsrState.initialized;
Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言');
} else {
_asrState.value = AsrState.error;
Logger.error('语音识别不可用');
}
} catch (e) {
_isAvailable.value = false;
_asrState.value = AsrState.error;
Logger.error('语音识别初始化失败: $e');
}
}
/// 处理语音识别状态变化
void _onSpeechStatus(String status) {
Logger.info('语音识别状态: $status');
switch (status) {
case 'listening':
_asrState.value = AsrState.listening;
_isListening = true;
break;
case 'notListening':
if (_asrState.value == AsrState.listening) {
_asrState.value = AsrState.processing;
}
_isListening = false;
break;
case 'done':
_asrState.value = AsrState.done;
_isListening = false;
// 在连续识别模式下,当一次识别完成后立即开始下一次识别
if (_continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition();
}
break;
}
}
/// 处理语音识别错误
void _onSpeechError(SpeechRecognitionError error) {
Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})');
_asrState.value = AsrState.error;
// 向连续识别控制器发送错误事件(如果存在)
_continuousRecognitionController?.add(
RecognitionEvent.error(error.errorMsg)
);
// 在连续识别模式下,尝试从错误中恢复(非永久性错误)
if (!error.permanent && _continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition();
}
}
/// 启动语音识别的统一方法
Future<bool> _startListening({
required Function(SpeechRecognitionResult) onResult,
Duration listenFor = const Duration(seconds: 30),
Duration pauseFor = const Duration(seconds: 3),
bool autoPunctuation = true,
}) async {
try {
// 如果当前正在监听,先停止
if (_speech.isListening) {
await _speech.stop();
// 短暂延迟确保资源释放
await Future.delayed(const Duration(milliseconds: 300));
}
// 启动语音识别
return await _speech.listen(
onResult: onResult,
localeId: _currentLocale.value,
listenFor: listenFor,
pauseFor: pauseFor,
listenOptions: stt.SpeechListenOptions(
partialResults: true,
autoPunctuation: autoPunctuation,
),
) ?? false;
} catch (e) {
Logger.error('启动语音识别失败: $e');
return false;
}
}
/// 重新启动连续识别
Future<void> _restartContinuousRecognition() async {
try {
// 确保控制器有效
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) {
Logger.info('连续识别控制器已关闭,跳过重启');
return;
}
// 短暂延迟后再重新启动
await Future.delayed(const Duration(milliseconds: 300));
// 使用统一方法启动识别
final success = await _startListening(
onResult: _onSpeechResult,
);
if (success) {
Logger.info('连续识别重启成功');
} else {
Logger.error('连续识别重启失败');
_continuousRecognitionController?.add(
RecognitionEvent.error('重启语音识别失败')
);
}
} catch (e) {
Logger.error('重启连续识别失败: $e');
}
}
/// 处理语音识别结果
void _onSpeechResult(SpeechRecognitionResult result) {
// 如果是连续识别,发送到连续识别控制器
if (_continuousRecognitionController != null) {
if (result.finalResult) {
Logger.info('收到最终识别结果: "${result.recognizedWords}"');
_continuousRecognitionController!.add(
RecognitionEvent.finalResult(
text: result.recognizedWords,
detectedLanguage: currentLocale,
)
);
} else {
_continuousRecognitionController!.add(
RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: result.recognizedWords,
detectedLanguage: currentLocale,
)
);
}
}
}
/// 设置当前语言
void setLocale(String localeId) {
if (_supportedLanguages.contains(localeId)) {
_currentLocale.value = localeId;
Logger.info('设置语音识别语言: $localeId');
} else {
Logger.error('不支持的语言: $localeId');
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 初始化语音识别
await _initSpeech();
return _isAvailable.value;
} catch (e) {
Logger.error('语音识别初始化失败: $e');
return false;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
try {
if (!_isAvailable.value) {
return RecognitionEvent.error('语音识别不可用');
}
if (_isListening) {
await stopContinuousRecognition();
}
// 创建Completer来等待最终结果
final completer = Completer<RecognitionEvent>();
// 开始监听
_asrState.value = AsrState.listening;
// 定义结果处理函数
void resultListener(SpeechRecognitionResult result) {
if (result.finalResult) {
// 只有在最终结果时才完成completer
completer.complete(RecognitionEvent.finalResult(
text: result.recognizedWords,
detectedLanguage: currentLocale,
));
}
}
// 使用统一方法启动识别,单次识别使用更短的时间参数
final success = await _startListening(
onResult: resultListener,
listenFor: const Duration(seconds: 15),
pauseFor: const Duration(seconds: 2),
);
if (!success) {
return RecognitionEvent.error('启动语音识别失败');
}
// 设置较短的超时时间
return await completer.future.timeout(
const Duration(seconds: 20), // 略长于listenFor以确保获取到结果
onTimeout: () {
_speech.stop();
return RecognitionEvent.error('语音识别超时');
},
);
} catch (e) {
Logger.error('一次性语音识别失败: $e');
return RecognitionEvent.error('$e');
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
try {
if (!_isAvailable.value) {
final controller = StreamController<RecognitionEvent>.broadcast();
controller.add(RecognitionEvent.error('语音识别不可用'));
controller.close();
return controller.stream;
}
// 如果已经在监听,先停止
if (_isListening) {
await stopContinuousRecognition();
}
// 创建新的流控制器
_continuousRecognitionController = StreamController<RecognitionEvent>.broadcast();
// 开始会话
_continuousRecognitionController!.add(
RecognitionEvent(type: RecognitionEventType.sessionStarted)
);
// 开始连续监听
_asrState.value = AsrState.listening;
// 使用统一方法启动识别,连续识别使用较长的参数
final success = await _startListening(
onResult: _onSpeechResult,
);
if (!success) {
// 添加错误事件,但保留流
_continuousRecognitionController!.add(
RecognitionEvent.error('启动连续语音识别失败')
);
// 返回已有的流控制器
return _continuousRecognitionController!.stream;
}
_isListening = true;
return _continuousRecognitionController!.stream;
} catch (e) {
Logger.error('启动连续语音识别失败: $e');
// 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) {
_continuousRecognitionController!.add(RecognitionEvent.error('$e'));
return _continuousRecognitionController!.stream;
} else {
// 否则创建新的控制器
final controller = StreamController<RecognitionEvent>.broadcast();
controller.add(RecognitionEvent.error('$e'));
return controller.stream;
}
}
}
@override
Future<bool> stopContinuousRecognition() async {
try {
// 无论是否正在监听,都尝试停止
if (_speech.isListening) {
await _speech.stop();
}
_isListening = false;
// 发送会话结束事件
_continuousRecognitionController?.add(
RecognitionEvent(type: RecognitionEventType.sessionStopped)
);
// 关闭流控制器
await _continuousRecognitionController?.close();
_continuousRecognitionController = null;
_asrState.value = AsrState.done;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
// 强制重置状态
_isListening = false;
_asrState.value = AsrState.error;
return false;
}
}
@override
bool isContinuousRecognitionActive() {
return _isListening;
}
@override
Future<void> dispose() async {
try {
// 确保停止所有识别活动
if (_speech.isListening) {
await _speech.stop();
}
// 关闭控制器
await _continuousRecognitionController?.close();
_continuousRecognitionController = null;
_isListening = false;
_asrState.value = AsrState.notInitialized;
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
}
}
}

484
lib/data/services/speech_impl/flutter_tts_service.dart

@ -0,0 +1,484 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:flutter_tts/flutter_tts.dart';
import '../../../core/utils/logger.dart';
import '../tts_service.dart';
/// Flutter TTS服务,负责基本的文本转语音功能
///
/// 使用flutter_tts插件实现TtsService接口
class FlutterTtsService extends GetxService implements TtsService {
static final FlutterTtsService to = Get.put(FlutterTtsService());
// flutter_tts实例
late final FlutterTts _flutterTts;
// 支持的语音列表
final List<String> _supportedVoices = [];
List<String> get supportedVoices => _supportedVoices;
// 当前语音
final RxString _currentVoice = ''.obs;
@override
String get currentVoice => _currentVoice.value;
// 播放状态
final RxBool _isSpeaking = false.obs;
@override
bool get isSpeaking => _isSpeaking.value;
// 文本队列
final Queue<_SpeechItem> _textQueue = Queue<_SpeechItem>();
bool _isProcessingQueue = false;
// 事件流控制器
final StreamController<TtsEvent> _eventController = StreamController<TtsEvent>.broadcast();
@override
Stream<TtsEvent> get onEvent => _eventController.stream;
// 语言
String _language = 'zh-CN';
// 流式文本缓冲区
String _streamBuffer = '';
// 当前参数设置
double _volume = 1.0;
double _rate = 0.5;
double _pitch = 1.0;
FlutterTtsService() {
_flutterTts = FlutterTts();
_initTts();
}
/// 初始化TTS
Future<void> _initTts() async {
try {
// 设置完成回调
_flutterTts.setCompletionHandler(() {
_isSpeaking.value = false;
_eventController.add(TtsEvent(type: TtsEventType.completed));
});
// 设置开始回调
_flutterTts.setStartHandler(() {
_isSpeaking.value = true;
_eventController.add(TtsEvent(type: TtsEventType.started));
});
// 设置错误回调
_flutterTts.setErrorHandler((msg) {
_isSpeaking.value = false;
_eventController.add(TtsEvent.error(msg));
Logger.error('TTS错误: $msg');
});
// 设置取消回调
_flutterTts.setCancelHandler(() {
_isSpeaking.value = false;
_eventController.add(TtsEvent(type: TtsEventType.canceled));
});
// 设置默认语言
await _flutterTts.setLanguage(_language);
// 设置音量
await _flutterTts.setVolume(_volume);
// 设置语速
await _flutterTts.setSpeechRate(_rate);
// 设置音调
await _flutterTts.setPitch(_pitch);
Logger.info('FlutterTtsService 已初始化');
} catch (e) {
Logger.error('TTS初始化失败: $e');
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
// 获取可用的语音
await _loadVoices();
// 更新支持的语言列表(如果提供)
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_language = supportedLanguages.first;
await _flutterTts.setLanguage(_language);
}
Logger.info('FlutterTtsService 初始化成功');
Logger.info('支持的语音: ${_supportedVoices.join(", ")}');
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
return false;
}
}
/// 加载可用的语音
Future<void> _loadVoices() async {
try {
_supportedVoices.clear();
// 获取可用的语音
final voices = await _flutterTts.getVoices;
if (voices != null && voices is List) {
for (final voice in voices) {
if (voice is Map && voice.containsKey('name')) {
_supportedVoices.add(voice['name'].toString());
}
}
}
// 如果没有找到语音,添加默认语音
if (_supportedVoices.isEmpty) {
_supportedVoices.add('default');
}
// 设置默认语音
if (_currentVoice.value.isEmpty && _supportedVoices.isNotEmpty) {
_currentVoice.value = _supportedVoices.first;
}
} catch (e) {
Logger.error('加载TTS语音失败: $e');
// 添加默认语音
if (_supportedVoices.isEmpty) {
_supportedVoices.add('default');
_currentVoice.value = 'default';
}
}
}
@override
Future<bool> setVoice(String voiceName) async {
try {
// 检查语音是否存在
if (!_supportedVoices.contains(voiceName) && voiceName != 'default') {
Logger.warning('语音不存在: $voiceName,使用默认语音');
voiceName = _supportedVoices.isNotEmpty ? _supportedVoices.first : 'default';
}
// 设置语音
await _flutterTts.setVoice({"name": voiceName});
_currentVoice.value = voiceName;
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
return false;
}
}
/// 设置语音参数
Future<bool> setSpeechParams({double? rate, double? pitch, double? volume}) async {
try {
if (volume != null) {
await _flutterTts.setVolume(volume);
_volume = volume;
}
if (rate != null) {
await _flutterTts.setSpeechRate(rate);
_rate = rate;
}
if (pitch != null) {
await _flutterTts.setPitch(pitch);
_pitch = pitch;
}
return true;
} catch (e) {
Logger.error('设置语音参数失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e'));
return false;
}
}
@override
Future<bool> speakOnce(String text) async {
if (text.isEmpty) return false;
try {
// 如果正在播放,先停止
if (_isSpeaking.value) {
await stop();
}
// 播放文本
await _flutterTts.speak(text);
return true;
} catch (e) {
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
Logger.error('TTS播放失败: $e');
return false;
}
}
@override
Future<bool> speakStream(String text) async {
if (text.isEmpty) return false;
try {
// 添加文本到缓冲区
_streamBuffer += text;
// 如果缓冲区为空,直接返回
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: _rate,
pitch: _pitch,
volume: _volume,
));
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
return false;
}
}
@override
Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try {
// 处理缓冲区中可能的完整句子
await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem(
text: _streamBuffer,
rate: _rate,
pitch: _pitch,
volume: _volume,
));
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成
while (_isProcessingQueue || _isSpeaking.value) {
await Future.delayed(const Duration(milliseconds: 100));
}
Logger.info('TTS队列播放完成');
return true;
} catch (e) {
Logger.error('等待TTS队列播放完成失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
return false;
}
}
/// 处理队列中的文本
Future<void> _processQueue() async {
if (_isProcessingQueue) return;
_isProcessingQueue = true;
try {
while (_textQueue.isNotEmpty) {
if (!_isSpeaking.value) {
final item = _textQueue.removeFirst();
// 如果参数有变化,更新参数
if (item.rate != _rate || item.pitch != _pitch || item.volume != _volume) {
await setSpeechParams(
rate: item.rate,
pitch: item.pitch,
volume: item.volume,
);
}
// 播放文本
await _flutterTts.speak(item.text);
// 等待开始播放
int attempts = 0;
while (!_isSpeaking.value && attempts < 10) {
await Future.delayed(const Duration(milliseconds: 50));
attempts++;
}
// 等待播放完成
while (_isSpeaking.value) {
await Future.delayed(const Duration(milliseconds: 100));
}
} else {
// 等待一小段时间再检查队列
await Future.delayed(const Duration(milliseconds: 100));
}
}
} catch (e) {
Logger.error('TTS处理队列失败: $e');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
} finally {
_isProcessingQueue = false;
}
}
@override
Future<bool> stop() async {
try {
Logger.info('尝试停止TTS播放...');
// 清空队列
_textQueue.clear();
_streamBuffer = '';
// 如果正在播放,停止播放
if (_isSpeaking.value) {
// 先重置状态标志,避免处理队列循环继续等待
_isProcessingQueue = false;
// 执行停止
final result = await _flutterTts.stop();
Logger.info('TTS停止结果: $result');
// 短暂延迟等待状态更新
await Future.delayed(const Duration(milliseconds: 100));
// 强制更新状态
_isSpeaking.value = false;
// 发送取消事件
_eventController.add(TtsEvent(type: TtsEventType.canceled));
}
Logger.info('TTS停止播放完成');
return true;
} catch (e) {
Logger.error('TTS停止播放失败: $e');
// 即使出错也尝试强制重置状态
_isSpeaking.value = false;
_isProcessingQueue = false;
return false;
}
}
@override
Future<void> dispose() async {
try {
await stop();
_textQueue.clear();
// 关闭事件流控制器
await _eventController.close();
Logger.info('TTS资源已释放');
} catch (e) {
Logger.error('释放TTS资源失败: $e');
}
}
/// 处理AI返回的文本流,将其转换为语音
///
/// [textStream] 文本流
/// 返回一个Future,当流处理完成时完成
Future<bool> processAiTextStream(Stream<String> textStream) async {
try {
await for (final text in textStream) {
if (text.isEmpty) continue;
await speakStream(text);
}
// 处理完流后,刷新缓冲区中的剩余文本
await flushStream();
return true;
} catch (e) {
Logger.error('处理文本流时出错: $e');
return false;
}
}
}
/// 语音合成项目
class _SpeechItem {
final String text;
final double rate;
final double pitch;
final double volume;
_SpeechItem({
required this.text,
this.rate = 0.5,
this.pitch = 1.0,
this.volume = 1.0,
});
}

807
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -0,0 +1,807 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../core/utils/logger.dart';
import 'package:record/record.dart';
import '../asr_service.dart';
/// Volcano语音识别服务
///
/// 负责与火山引擎实时语音识别服务的通信
class VolcanoAsrApiService implements AsrService {
static final VolcanoAsrApiService to = Get.find<VolcanoAsrApiService>();
// API配置
late final String _appKey;
late final String _secretKey;
late final String _serviceId;
late final String _hostUrl;
final String _path = '/ws/v1/asr';
// 请求相关参数
final String _algorithm = 'hmac-sha1';
final Uuid _uuid = const Uuid();
// 录音配置
final int _sampleRate = 16000;
final int _bitsPerSample = 16;
final int _channels = 1;
final String _encoding = 'pcm';
// 连接相关
WebSocketChannel? _channel;
Completer<void>? _connectionCompleter;
StreamSubscription? _connectionSubscription;
Timer? _heartbeatTimer;
// 语音录音
final Record? _recorder = null; // TODO: 实现正确的Record实例化
Timer? _audioTimer;
bool _isRecording = false;
// 状态控制
bool _isInitialized = false;
bool _isConnecting = false;
bool _isContinuousRecognitionActive = false;
// 识别结果控制
bool _isRecognitionEndRequested = false;
String _currentSessionId = '';
final List<String> _supportedLanguages = ['zh-CN', 'en-US'];
String _selectedLanguage = 'zh-CN';
// 结果流
StreamController<RecognitionEvent>? _recognitionController;
// 事件计数器(用于调试)
int _sendPacketCount = 0;
int _receivePacketCount = 0;
// AsrService接口实现
@override
List<String> get supportedLanguages => _supportedLanguages;
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
// 如果已经初始化过,先释放资源
if (_isInitialized) {
await dispose();
}
_isInitialized = false;
// 从环境变量加载配置
_loadConfig();
// 设置支持的语言
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages);
}
// 初始化录音机
await _initRecorder();
_isInitialized = true;
Logger.info('火山语音API已初始化');
return true;
} catch (e) {
Logger.error('初始化火山语音API失败: $e');
return false;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
try {
if (!_isInitialized) {
await initialize();
}
final resultCompleter = Completer<RecognitionEvent>();
// 开始连续识别流程
final stream = await startContinuousRecognition();
// 监听结果
StreamSubscription? subscription;
subscription = stream.listen((event) {
if (event.type == RecognitionEventType.finalResult) {
// 收到最终结果,完成单次识别
subscription?.cancel();
resultCompleter.complete(event);
stopContinuousRecognition();
} else if (event.type == RecognitionEventType.error) {
// 发生错误,返回错误
subscription?.cancel();
resultCompleter.completeError(event.error);
stopContinuousRecognition();
}
}, onError: (e) {
subscription?.cancel();
resultCompleter.completeError(e.toString());
stopContinuousRecognition();
}, onDone: () {
if (!resultCompleter.isCompleted) {
resultCompleter.complete(RecognitionEvent.error('未收到识别结果'));
}
});
// 设置超时
Timer(const Duration(seconds: 30), () {
if (!resultCompleter.isCompleted) {
subscription?.cancel();
resultCompleter.complete(RecognitionEvent.error('识别超时'));
stopContinuousRecognition();
}
});
return resultCompleter.future;
} catch (e) {
Logger.error('单次语音识别失败: $e');
return RecognitionEvent.error(e.toString());
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
}
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
try {
// 创建结果控制器
_recognitionController = StreamController<RecognitionEvent>.broadcast();
// 连接WebSocket
await _connectWebSocket();
// 开始录音
await _startRecording();
_isContinuousRecognitionActive = true;
_isRecognitionEndRequested = false;
Logger.info('连续语音识别已开始');
return _recognitionController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
await _cleanupResources();
// 返回一个包含错误的流
final errorController = StreamController<RecognitionEvent>.broadcast();
errorController.add(RecognitionEvent.error(e.toString()));
errorController.close();
return errorController.stream;
}
}
@override
Future<bool> stopContinuousRecognition() async {
if (!_isContinuousRecognitionActive) {
return true;
}
try {
_isRecognitionEndRequested = true;
// 先停止录音,以确保不会再发送新的音频数据
await _stopRecording();
// 发送结束指令
await _sendEndSignal();
// 等待服务器处理完所有已发送的数据
await Future.delayed(const Duration(milliseconds: 500));
// 清理资源
await _cleanupResources();
_isContinuousRecognitionActive = false;
Logger.info('连续语音识别已停止');
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
// 确保资源被清理
await _cleanupResources();
_isContinuousRecognitionActive = false;
return false;
}
}
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
@override
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 确保资源被清理
await _cleanupResources();
_isInitialized = false;
Logger.info('火山语音API资源已释放');
} catch (e) {
Logger.error('释放火山语音API资源失败: $e');
_isContinuousRecognitionActive = false;
_isInitialized = false;
}
}
/// 从环境变量加载配置
void _loadConfig() {
_appKey = dotenv.env['VOLCANO_APP_KEY'] ?? '';
_secretKey = dotenv.env['VOLCANO_SECRET_KEY'] ?? '';
_serviceId = dotenv.env['VOLCANO_SERVICE_ID'] ?? '';
_hostUrl = dotenv.env['VOLCANO_HOST_URL'] ?? 'ws-api-asr.volces.com';
if (_appKey.isEmpty || _secretKey.isEmpty || _serviceId.isEmpty) {
throw Exception('未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID');
}
}
/// 初始化录音机
Future<void> _initRecorder() async {
try {
// TODO: 正确实现录音机初始化
// _recorder = Record();
// 检查和请求权限
// final hasPermission = await _recorder!.hasPermission();
// if (!hasPermission) {
// throw Exception('没有录音权限');
// }
Logger.info('录音机初始化成功');
} catch (e) {
// _recorder = null;
Logger.error('初始化录音机失败: $e');
rethrow;
}
}
/// 连接WebSocket
Future<void> _connectWebSocket() async {
if (_isConnecting) {
// 已经有一个连接进程在进行中
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
return _connectionCompleter!.future;
}
}
_connectionCompleter = Completer<void>();
_isConnecting = true;
try {
// 关闭现有连接
await _closeWebSocket();
// 生成新的会话ID
_currentSessionId = _uuid.v4();
// 构建URL和请求头
final (url, headers) = _buildConnectionParams();
// 创建WebSocket连接
if (kIsWeb) {
_channel = WebSocketChannel.connect(Uri.parse(url));
} else {
_channel = IOWebSocketChannel.connect(
Uri.parse(url),
headers: headers,
);
}
// 处理WebSocket消息
_connectionSubscription = _channel!.stream.listen(
_handleWebSocketMessage,
onError: _handleWebSocketError,
onDone: _handleWebSocketDone,
);
// 开始心跳
_startHeartbeat();
// 发送开始识别指令
await _sendStartCommand();
_isConnecting = false;
_connectionCompleter!.complete();
Logger.info('WebSocket连接成功');
} catch (e) {
_isConnecting = false;
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(e);
}
Logger.error('连接WebSocket失败: $e');
rethrow;
}
return _connectionCompleter!.future;
}
/// 构建连接参数
(String, Map<String, String>?) _buildConnectionParams() {
final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor();
final nonce = _generateNonce();
// 构建表单(用于签名和查询参数)
final form = {
'algorithm': _algorithm,
'app_key': _appKey,
'nonce': nonce,
'service_id': _serviceId,
'session_id': _currentSessionId,
'timestamp': timestamp.toString(),
};
// 计算签名
final signature = _calculateSignature(form);
// 添加签名到表单
form['signature'] = signature;
// 构建URL查询参数
final queryString = form.entries
.map((e) => '${e.key}=${Uri.encodeComponent(e.value)}')
.join('&');
// 构建完整URL
final url = 'wss://$_hostUrl$_path?$queryString';
// 构建请求头
final headers = <String, String>{
'User-Agent': 'Deep Voice App/1.0.0',
};
return (url, kIsWeb ? null : headers);
}
/// 生成随机数
String _generateNonce() {
final random = Random();
final randoms = List.generate(16, (_) => random.nextInt(256));
return base64Url.encode(randoms).substring(0, 16);
}
/// 计算签名
String _calculateSignature(Map<String, String> form) {
// 按键排序
final sortedKeys = form.keys.toList()..sort();
// 构建待签名字符串
final signString = sortedKeys.map((key) => '$key=${form[key]}').join('');
// 使用HMAC-SHA1进行签名
final key = utf8.encode(_secretKey);
final bytes = utf8.encode(signString);
final hmacSha1 = Hmac(sha1, key);
final digest = hmacSha1.convert(bytes);
// 返回Base64编码的签名
return base64.encode(digest.bytes);
}
/// 开始心跳定时
void _startHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (_) {
_sendHeartbeat();
});
}
/// 发送心跳包
void _sendHeartbeat() {
if (_channel != null && _isContinuousRecognitionActive) {
try {
final heartbeatMsg = {
'type': 'heartbeat',
'sequence': _sendPacketCount++,
};
_channel!.sink.add(jsonEncode(heartbeatMsg));
} catch (e) {
Logger.error('发送心跳包失败: $e');
}
}
}
/// 发送开始识别指令
Future<void> _sendStartCommand() async {
if (_channel == null) return;
try {
final startCommand = {
'type': 'start',
'sequence': _sendPacketCount++,
'asr_params': {
'sample_rate': _sampleRate,
'bits_per_sample': _bitsPerSample,
'channel': _channels,
'encoding': _encoding,
'audio_format': 'raw',
'lang': _selectedLanguage,
'vad_enable': true,
'vad_pause_time': 500,
'max_seconds': 60,
},
};
_channel!.sink.add(jsonEncode(startCommand));
Logger.info('已发送开始识别指令');
} catch (e) {
Logger.error('发送开始识别指令失败: $e');
rethrow;
}
}
/// 开始录音
Future<void> _startRecording() async {
// TODO: 实现正确的录音开始逻辑
/*
if (_recorder == null) {
await _initRecorder();
}
if (_isRecording) {
await _stopRecording();
}
try {
// 开始录音
await _recorder!.start(
encoder: AudioEncoder.pcm16bits,
samplingRate: _sampleRate,
numChannels: _channels,
);
_isRecording = true;
// 设置定时器定期获取音频数据
_audioTimer = Timer.periodic(const Duration(milliseconds: 200), (_) async {
await _captureAndSendAudio();
});
Logger.info('录音已开始');
} catch (e) {
_isRecording = false;
Logger.error('开始录音失败: $e');
rethrow;
}
*/
// 临时实现,防止错误
_isRecording = true;
Logger.info('录音已开始(模拟)');
}
/// 捕获并发送音频数据
Future<void> _captureAndSendAudio() async {
// TODO: 实现正确的音频捕获逻辑
/*
if (!_isRecording || _recorder == null || _channel == null) {
return;
}
try {
Uint8List? audioData;
if (Platform.isIOS) {
// iOS 使用文件缓冲区
final path = await _recorder!.stop();
if (path == null) return;
// 读取文件内容
final file = File(path);
if (await file.exists()) {
audioData = await file.readAsBytes();
// 重新开始录音
await _recorder!.start(
encoder: AudioEncoder.pcm16bits,
samplingRate: _sampleRate,
numChannels: _channels,
);
}
} else {
// Android 使用stream获取PCM数据
// 还未实现
}
// 如果有音频数据,发送给服务器
if (audioData != null && audioData.isNotEmpty) {
await _sendAudioData(audioData);
}
} catch (e) {
Logger.error('捕获和发送音频失败: $e');
}
*/
}
/// 发送音频数据
Future<void> _sendAudioData(Uint8List data) async {
if (_channel == null || data.isEmpty) return;
try {
final audioMessage = {
'type': 'audio',
'sequence': _sendPacketCount++,
'audio': base64.encode(data),
};
_channel!.sink.add(jsonEncode(audioMessage));
} catch (e) {
Logger.error('发送音频数据失败: $e');
}
}
/// 发送结束信号
Future<void> _sendEndSignal() async {
if (_channel == null) return;
try {
final endCommand = {
'type': 'end',
'sequence': _sendPacketCount++,
};
_channel!.sink.add(jsonEncode(endCommand));
Logger.info('已发送结束指令');
} catch (e) {
Logger.error('发送结束指令失败: $e');
}
}
/// 停止录音
Future<void> _stopRecording() async {
_audioTimer?.cancel();
_audioTimer = null;
// TODO: 实现正确的录音停止逻辑
/*
if (_isRecording && _recorder != null) {
try {
await _recorder!.stop();
_isRecording = false;
Logger.info('录音已停止');
} catch (e) {
Logger.error('停止录音失败: $e');
}
}
*/
// 临时实现,防止错误
_isRecording = false;
Logger.info('录音已停止(模拟)');
}
/// 处理WebSocket消息
void _handleWebSocketMessage(dynamic message) {
try {
_receivePacketCount++;
if (message is String) {
final Map<String, dynamic> data = jsonDecode(message);
final String type = data['type'] as String? ?? '';
switch (type) {
case 'result':
_handleResultMessage(data);
break;
case 'start_result':
_handleStartResultMessage(data);
break;
case 'end_result':
_handleEndResultMessage(data);
break;
case 'error':
_handleErrorMessage(data);
break;
case 'heartbeat_result':
// 心跳响应,不需要特殊处理
break;
default:
Logger.info('收到未知类型的消息: $type');
}
}
} catch (e) {
Logger.error('处理WebSocket消息失败: $e');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError('处理消息失败: $e');
}
}
}
/// 处理结果消息
void _handleResultMessage(Map<String, dynamic> data) {
try {
final Map<String, dynamic>? result = data['result'] as Map<String, dynamic>?;
if (result == null) return;
final bool isEnd = result['is_end'] as bool? ?? false;
final String text = result['text'] as String? ?? '';
if (_recognitionController != null && !_recognitionController!.isClosed) {
if (isEnd) {
// 最终结果
_recognitionController!.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
));
} else {
// 中间结果
_recognitionController!.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: text,
));
}
}
} catch (e) {
Logger.error('处理结果消息失败: $e');
}
}
/// 处理开始结果消息
void _handleStartResultMessage(Map<String, dynamic> data) {
final int code = data['code'] as int? ?? -1;
if (code == 0) {
// 开始成功
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
}
} else {
// 开始失败
final String message = data['message'] as String? ?? '未知错误';
Logger.error('语音识别开始失败: [$code] $message');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.add(RecognitionEvent.error('开始失败: [$code] $message'));
}
}
}
/// 处理结束结果消息
void _handleEndResultMessage(Map<String, dynamic> data) {
final int code = data['code'] as int? ?? -1;
if (code == 0) {
// 结束成功
Logger.info('语音识别成功结束');
} else {
// 结束失败
final String message = data['message'] as String? ?? '未知错误';
Logger.error('语音识别结束失败: [$code] $message');
}
// 无论成功还是失败,通知会话已结束
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
}
// 清理资源
_cleanupResources();
}
/// 处理错误消息
void _handleErrorMessage(Map<String, dynamic> data) {
final int code = data['code'] as int? ?? -1;
final String message = data['message'] as String? ?? '未知错误';
Logger.error('收到错误消息: [$code] $message');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.add(RecognitionEvent.error('[$code] $message'));
}
}
/// 处理WebSocket错误
void _handleWebSocketError(dynamic error) {
Logger.error('WebSocket错误: $error');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(error);
}
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError(error);
}
// 清理资源
_cleanupResources();
}
/// 处理WebSocket连接关闭
void _handleWebSocketDone() {
Logger.info('WebSocket连接已关闭');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError('WebSocket连接已关闭');
}
// 通知会话已结束
if (_recognitionController != null && !_recognitionController!.isClosed && !_isRecognitionEndRequested) {
_recognitionController!.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
}
// 清理资源
_cleanupResources();
}
/// 关闭WebSocket连接
Future<void> _closeWebSocket() async {
_connectionSubscription?.cancel();
_connectionSubscription = null;
_heartbeatTimer?.cancel();
_heartbeatTimer = null;
if (_channel != null) {
try {
_channel!.sink.close();
_channel = null;
} catch (e) {
Logger.error('关闭WebSocket连接失败: $e');
}
}
}
/// 清理所有资源
Future<void> _cleanupResources() async {
// 停止录音
await _stopRecording();
// 关闭WebSocket
await _closeWebSocket();
// 关闭结果控制器
if (_recognitionController != null && !_recognitionController!.isClosed) {
await _recognitionController!.close();
_recognitionController = null;
}
_isContinuousRecognitionActive = false;
_isRecognitionEndRequested = false;
}
}

2
lib/data/services/volcano_tts_api_service.dart → lib/data/services/speech_impl/volcano_tts_api_service.dart

@ -11,7 +11,7 @@ import 'package:web_socket_channel/io.dart';
import 'package:uuid/uuid.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import '../../core/utils/logger.dart';
import '../../../core/utils/logger.dart';
/// 火山语音合成双向流式API服务
///

83
lib/data/services/tts_service.dart

@ -0,0 +1,83 @@
import 'dart:async';
/// 语音合成服务接口
abstract class TtsService {
/// 当前选择的语音
String get currentVoice;
/// 当前是否正在播放
bool get isSpeaking;
/// TTS事件流,用于监听语音合成事件
Stream<TtsEvent> get onEvent;
/// 初始化语音合成服务
Future<bool> initialize({
List<String>? supportedLanguages,
});
/// 设置语音
Future<bool> setVoice(String voiceName);
/// 单次播放文本,完成后返回
Future<bool> speakOnce(String text);
/// 将文本加入播放队列,支持连续播放多段文本
Future<bool> speakStream(String text);
/// 播放队列中剩余文本
Future<bool> flushStream();
/// 停止播放
Future<bool> stop();
/// 释放资源
Future<void> dispose();
}
/// TTS事件类型
enum TtsEventType {
/// 开始播放
started,
/// 播放完成
completed,
/// 播放取消
canceled,
/// 播放错误
error,
}
/// TTS事件
class TtsEvent {
/// 事件类型
final TtsEventType type;
/// 错误信息(仅在 error 类型中有效)
final String error;
TtsEvent({
required this.type,
this.error = '',
});
/// 创建错误事件的快捷构造函数
factory TtsEvent.error(String errorMessage) {
return TtsEvent(
type: TtsEventType.error,
error: errorMessage,
);
}
/// 检查是否为错误
bool get isError => type == TtsEventType.error;
@override
String toString() {
return 'TtsEvent{type: $type, error: $error}';
}
}

6
lib/data/services/voice_interaction_service.dart

@ -5,8 +5,10 @@ import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../modules/chat/models/message_model.dart';
import 'chat_history_service.dart';
import '../../data/services/azure_asr_service.dart';
import '../../data/services/azure_tts_service.dart';
import 'speech_impl/azure_asr_service.dart';
import 'speech_impl/azure_tts_service.dart';
import 'asr_service.dart';
import 'tts_service.dart';
/// 语音交互服务
///

1003
lib/data/services/volcano_asr_api_service.dart

File diff suppressed because it is too large

743
lib/data/services/volcano_tts_service.dart

@ -1,743 +0,0 @@
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,29 +1,15 @@
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/speech_impl/azure_asr_service.dart';
import '../../../data/services/speech_impl/azure_tts_service.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/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());

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

@ -3,19 +3,18 @@ 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/azure_tts_service.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../data/models/agent.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/chat_history_service.dart';
import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/asr_service.dart';
class ChatController extends GetxController {
// 服务
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
final AzureAsrService _asrService = Get.find<AzureAsrService>();
late TtsService _ttsService;
late AsrService _asrService;
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>();
@ -58,6 +57,7 @@ class ChatController extends GetxController {
super.onInit();
Logger.info('ChatController onInit');
// 从路由参数获取Agent信息
final Map<String, dynamic> args = Get.arguments ?? {};
final String agentId = args['agentId'] ?? 'personal_assistant';
@ -72,6 +72,9 @@ class ChatController extends GetxController {
agent = foundAgent;
_ttsService = Get.find<TtsService>();
_asrService = Get.find<AsrService>();
// 设置TTS服务
await _ttsService.setVoice(agent.voice);
@ -308,7 +311,7 @@ class ChatController extends GetxController {
// 如果TTS启用,使用流式处理发送到TTS
if (isTtsEnabled.value) {
_ttsService.processStreamText(chunk);
_ttsService.speakStream(chunk);
}
},
onDone: () {
@ -319,7 +322,7 @@ class ChatController extends GetxController {
// 如果TTS启用,处理缓冲区中的剩余文本
if (isTtsEnabled.value) {
_ttsService.flushStreamBuffer();
_ttsService.flushStream();
}
// 保存聊天历史

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

@ -489,7 +489,7 @@ class ChatView extends GetView<ChatController> {
),
child: SafeArea(
child: SizedBox(
height: 120.h,
height: 80.h,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(

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

@ -3,7 +3,6 @@ import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../data/services/audio_service.dart';
import '../../../routes/app_routes.dart';
class ProfileView extends GetView<ProfileController> {

39
lib/modules/settings/controllers/settings_controller.dart

@ -1,6 +1,7 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../../core/themes/theme_service.dart';
import '../../../data/services/speech_factory.dart';
class SettingsController extends GetxController {
// 主题服务
@ -14,6 +15,13 @@ class SettingsController extends GetxController {
final RxString targetLanguage = '英文'.obs;
final List<String> availableLanguages = ['中文', '英文', '日文', '韩文', '法文', '德文', '西班牙文', '俄文'];
// 语音服务类型
final Rx<SpeechServiceType> speechServiceType = SpeechServiceType.azure.obs;
final List<Map<String, dynamic>> availableSpeechServices = [
{'type': SpeechServiceType.azure, 'name': 'Azure 语音服务', 'description': '微软云端语音服务,支持高质量语音合成和识别'},
{'type': SpeechServiceType.flutter, 'name': 'Flutter 本地语音服务', 'description': '设备本地语音服务,无需联网即可使用'},
];
@override
void onInit() {
super.onInit();
@ -23,10 +31,23 @@ class SettingsController extends GetxController {
// 设置当前主题状态
isDarkMode.value = savedDarkMode;
// 获取当前语音服务类型
_loadCurrentSpeechServiceType();
}
// 加载当前语音服务类型
void _loadCurrentSpeechServiceType() {
try {
final factory = Get.find<SpeechFactory>();
speechServiceType.value = factory.currentServiceType;
} catch (e) {
// 如果找不到SpeechFactory实例,使用默认值
}
}
// 切换主题
void toggleTheme() {
Future<void> toggleTheme() async {
// 切换暗黑模式状态
isDarkMode.value = !isDarkMode.value;
@ -34,10 +55,18 @@ class SettingsController extends GetxController {
Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
// 保存用户选择的主题设置 - 确保立即写入存储
_themeService.saveThemeMode(isDarkMode.value);
// 打印日志,确认主题已保存
print('主题已切换,当前模式: ${isDarkMode.value ? "暗黑" : "浅色"}');
await _themeService.saveThemeMode(isDarkMode.value);
}
// 设置语音服务类型
void setSpeechServiceType(SpeechServiceType type) {
try {
final factory = Get.find<SpeechFactory>();
factory.setServiceType(type);
speechServiceType.value = type;
} catch (e) {
// 处理错误
}
}
void setSourceLanguage(String language) {

483
lib/modules/settings/views/settings_view.dart

@ -113,6 +113,7 @@ class SettingsView extends GetView<SettingsController> {
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Row(
@ -275,6 +276,11 @@ class SettingsView extends GetView<SettingsController> {
),
Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]),
// 语音服务选择
_buildSpeechServiceSetting(isDarkMode),
Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]),
// 隐私设置
_buildSimpleNavigationSetting(
title: '隐私设置',
@ -498,46 +504,52 @@ class SettingsView extends GetView<SettingsController> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
Expanded(
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(16.r),
),
if (subtitle != null)
SizedBox(height: 2.h),
if (subtitle != null)
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
if (subtitle != null)
SizedBox(height: 2.h),
if (subtitle != null)
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
Switch(
value: value,
@ -568,54 +580,64 @@ class SettingsView extends GetView<SettingsController> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
Expanded(
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(16.r),
),
if (subtitle != null)
SizedBox(height: 2.h),
if (subtitle != null)
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
if (subtitle != null)
SizedBox(height: 2.h),
if (subtitle != null)
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: valueStyle ?? TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
Flexible(
child: Text(
value,
style: valueStyle ?? TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
SizedBox(width: 8.w),
@ -659,46 +681,52 @@ class SettingsView extends GetView<SettingsController> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: titleColor ?? (isDarkMode ? Colors.white : Colors.black87),
),
Expanded(
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: iconBgColor,
borderRadius: BorderRadius.circular(16.r),
),
if (subtitle != null)
SizedBox(height: 2.h),
if (subtitle != null)
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: subtitleColor ?? (isDarkMode ? Colors.white70 : Colors.black54),
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: titleColor ?? (isDarkMode ? Colors.white : Colors.black87),
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
if (subtitle != null)
SizedBox(height: 2.h),
if (subtitle != null)
Text(
subtitle,
style: TextStyle(
fontSize: 12.sp,
color: subtitleColor ?? (isDarkMode ? Colors.white70 : Colors.black54),
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
Container(
width: 32.w,
@ -718,4 +746,221 @@ class SettingsView extends GetView<SettingsController> {
),
);
}
// 语音服务设置项
Widget _buildSpeechServiceSetting(bool isDarkMode) {
return InkWell(
onTap: () {
_showSpeechServiceSelectionDialog(isDarkMode);
},
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.teal[900]!.withOpacity(0.3) : Colors.teal[100]!,
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
Icons.record_voice_over,
size: 16.w,
color: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!,
),
),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'语音服务',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 2.h),
Text(
'选择语音识别和合成服务',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() {
final serviceType = controller.speechServiceType.value;
final serviceName = controller.availableSpeechServices
.firstWhere((service) => service['type'] == serviceType)['name'];
return Flexible(
child: Text(
serviceName,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
);
}),
SizedBox(width: 8.w),
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
Icons.chevron_right,
size: 16.w,
color: isDarkMode ? Colors.white70 : Colors.grey[500],
),
),
],
),
],
),
),
);
}
// 显示语音服务选择对话框
void _showSpeechServiceSelectionDialog(bool isDarkMode) {
Get.dialog(
Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'选择语音服务',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 16.h),
...controller.availableSpeechServices.map((service) {
return Obx(() {
final isSelected = controller.speechServiceType.value == service['type'];
return InkWell(
onTap: () {
controller.setSpeechServiceType(service['type']);
Get.back();
},
child: Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isSelected
? (isDarkMode ? Colors.teal[900]!.withOpacity(0.3) : Colors.teal[50]!)
: (isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100]!),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isSelected
? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!)
: Colors.transparent,
width: 1,
),
),
child: Row(
children: [
Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: isSelected
? (isDarkMode ? Colors.teal[300] : Colors.teal[600])
: (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[300]),
shape: BoxShape.circle,
),
child: isSelected
? Icon(
Icons.check,
size: 16.w,
color: Colors.white,
)
: null,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
service['name'],
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Text(
service['description'],
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
],
),
),
],
),
),
);
});
}).toList(),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
],
),
],
),
),
),
);
}
}

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

@ -1,16 +0,0 @@
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());
}
}

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

@ -1,10 +0,0 @@
import 'package:get/get.dart';
import '../controllers/azure_tts_test_controller.dart';
class AzureTtsTestBinding extends Bindings {
@override
void dependencies() {
// Use put instead of lazyPut to ensure the controller is created immediately
Get.put<AzureTtsTestController>(AzureTtsTestController());
}
}

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

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

10
lib/modules/test/bindings/flutter_tts_test_binding.dart

@ -1,10 +0,0 @@
import 'package:get/get.dart';
import '../controllers/flutter_tts_test_controller.dart';
/// Flutter TTS测试绑定
class FlutterTtsTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<FlutterTtsTestController>(() => FlutterTtsTestController());
}
}

25
lib/modules/test/bindings/test_binding.dart

@ -1,25 +0,0 @@
import 'package:get/get.dart';
import '../controllers/volcano_tts_test_controller.dart';
import '../controllers/azure_asr_test_controller.dart';
import '../../../data/services/volcano_tts_api_service.dart';
class VolcanoTtsTestBinding extends Bindings {
@override
void dependencies() {
// Make sure the VolcanoTtsApiService is registered
if (!Get.isRegistered<VolcanoTtsApiService>()) {
Get.put(VolcanoTtsApiService(), permanent: true);
}
// Use put instead of lazyPut to ensure the controller is created immediately
Get.put<VolcanoTtsTestController>(VolcanoTtsTestController());
}
}
class AzureAsrTestBinding extends Bindings {
@override
void dependencies() {
// Use put instead of lazyPut to ensure the controller is created immediately
Get.put<AzureAsrTestController>(AzureAsrTestController());
}
}

10
lib/modules/test/bindings/volcano_asr_test_binding.dart

@ -1,10 +0,0 @@
import 'package:get/get.dart';
import '../controllers/volcano_asr_test_controller.dart';
class VolcanoAsrTestBinding extends Bindings {
@override
void dependencies() {
// Use put instead of lazyPut to ensure the controller is created immediately
Get.put<VolcanoAsrTestController>(VolcanoAsrTestController());
}
}

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

@ -1,298 +0,0 @@
import 'dart:async';
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 _azureAsrService = Get.find<AzureAsrService>();
// 状态变量
final RxBool isListening = false.obs;
final RxString recognizedText = ''.obs;
final RxString partialText = ''.obs;
final RxString detectedLanguage = ''.obs;
final Rx<AzureAsrState> asrState = AzureAsrState.notInitialized.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', // 俄语
];
// 自动语言检测
final RxBool isAutoDetectEnabled = false.obs;
final RxList<String> selectedSupportedLanguages = <String>['zh-CN', 'en-US'].obs;
// 订阅
StreamSubscription? _recognitionSubscription;
@override
void onInit() {
super.onInit();
_initializeAsr();
}
// 初始化ASR服务
Future<void> _initializeAsr() async {
try {
// 根据选择的支持语言初始化服务
final success = await _azureAsrService.initialize(
supportedLanguages: selectedSupportedLanguages,
);
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 (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition();
}
partialText.value = '';
final result = await _azureAsrService.recognizeSpeech();
if (result.text.isNotEmpty) {
recognizedText.value = result.text;
detectedLanguage.value = result.detectedLanguage;
}
} 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 {
await startContinuousRecognition();
}
} catch (e) {
Logger.error('切换连续语音识别出错: $e');
asrState.value = AzureAsrState.error;
}
}
// 开始连续语音识别
Future<void> startContinuousRecognition() async {
try {
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;
detectedLanguage.value = event.detectedLanguage;
}
partialText.value = '';
break;
case RecognitionEventType.intermediateResult:
if (event.text.isNotEmpty) {
partialText.value = event.text;
if (event.detectedLanguage.isNotEmpty) {
detectedLanguage.value = event.detectedLanguage;
}
}
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');
asrState.value = AzureAsrState.error;
isListening.value = false;
partialText.value = '';
}
}
// 停止连续语音识别
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();
}
// 确保新语言在选中支持的语言列表中
if (!selectedSupportedLanguages.contains(language)) {
// 添加新语言到支持列表
final newLanguages = [...selectedSupportedLanguages, language];
selectedSupportedLanguages.value = newLanguages;
// 重新初始化ASR服务
await _initializeAsr();
}
recognizedText.value = '';
partialText.value = '';
detectedLanguage.value = '';
asrState.value = AzureAsrState.initialized;
} catch (e) {
Logger.error('更改语言出错: $e');
asrState.value = AzureAsrState.error;
}
}
// 切换自动语言检测
Future<void> toggleAutoDetectLanguage() async {
try {
isAutoDetectEnabled.value = !isAutoDetectEnabled.value;
// 停止当前识别
if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition();
}
// 重新初始化ASR服务 - 自动语言检测现在通过选择多种语言实现
await _initializeAsr();
recognizedText.value = '';
partialText.value = '';
detectedLanguage.value = '';
} catch (e) {
Logger.error('切换自动语言检测出错: $e');
asrState.value = AzureAsrState.error;
}
}
// 更新支持的语言列表
Future<void> updateSupportedLanguages(List<String> languages) async {
try {
if (languages.isEmpty) return;
selectedSupportedLanguages.value = languages;
// 停止当前识别
if (_azureAsrService.isContinuousRecognitionActive()) {
await stopContinuousRecognition();
}
// 重新初始化ASR服务,使用新的语言列表
final success = await _azureAsrService.initialize(
supportedLanguages: languages,
);
asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error;
} catch (e) {
Logger.error('更新支持的语言列表出错: $e');
asrState.value = AzureAsrState.error;
}
}
@override
void onClose() {
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
if (_azureAsrService.isContinuousRecognitionActive()) {
_azureAsrService.stopContinuousRecognition();
}
super.onClose();
}
}

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

@ -1,279 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../../../data/services/azure_tts_service.dart';
import '../../../core/utils/logger.dart';
class AzureTtsTestController extends GetxController {
// 使用优化后的 AzureTtsService
final AzureTtsService _ttsService = Get.put(AzureTtsService());
// 可观察状态
final isSpeaking = false.obs;
final errorMessage = ''.obs;
final isLoading = false.obs;
final statusMessage = '未连接'.obs;
// 添加实际音频播放状态
final isAudioPlaying = false.obs;
// 播放状态检查定时器
Timer? _playbackCheckTimer;
// 当前播放的索引
final currentPlayingIndex = RxInt(-1);
// 语音选择相关
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 rate = 0.obs;
final pitch = 0.obs;
final volume = 100.obs;
// 固定的播报内容
final RxList<String> speechTexts = <String>[
'这是第一条Azure语音播报测试,请听取合成效果。',
'这是第二条Azure语音播报测试,Azure语音合成支持连续多条播报。',
'这是第三条Azure语音播报测试,可以测试语音合成的流畅度和连贯性。',
'这是第四条Azure语音播报测试,感谢您的使用。',
].obs;
@override
void onInit() {
super.onInit();
// 初始化UI状态
isSpeaking.value = false;
isAudioPlaying.value = false;
errorMessage.value = '';
isLoading.value = false;
// 监听TTS播放状态
ever(_ttsService.isSpeaking, (bool playing) {
isAudioPlaying.value = playing;
if (!playing && currentPlayingIndex.value >= 0) {
// 如果播放结束,重置当前播放索引
currentPlayingIndex.value = -1;
}
});
// 启动播放状态检查定时器
_startPlaybackCheckTimer();
// 初始化TTS服务
_initializeTtsService();
}
/// 初始化TTS服务
Future<void> _initializeTtsService() async {
try {
isLoading.value = true;
errorMessage.value = '';
statusMessage.value = '正在初始化...';
// 初始化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> changeVoice(String voice) async {
try {
isLoading.value = true;
errorMessage.value = '';
selectedVoice.value = voice;
// 设置语音
final result = await _ttsService.setVoice(voice);
if (result) {
statusMessage.value = '已设置语音: $voice';
} else {
errorMessage.value = '设置语音失败';
}
isLoading.value = false;
} catch (e) {
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,
);
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) {
return;
}
try {
isSpeaking.value = true;
isAudioPlaying.value = true;
errorMessage.value = '';
currentPlayingIndex.value = index;
statusMessage.value = '正在合成...';
final text = speechTexts[index];
// 播放文本
await _ttsService.speak(text);
statusMessage.value = '合成完成';
} catch (e) {
errorMessage.value = '播放出错';
statusMessage.value = '错误';
Logger.error('播放出错: $e');
} finally {
isSpeaking.value = false;
if (!_ttsService.isSpeaking.value) {
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
}
}
}
// 启动播放状态检查定时器
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 = true;
errorMessage.value = '';
statusMessage.value = '开始连续播放';
// 播放所有文本
await _ttsService.speakMultiple(speechTexts.toList());
statusMessage.value = '连续播放完成';
} catch (e) {
errorMessage.value = '连续播放出错';
statusMessage.value = '错误';
Logger.error('连续播放出错: $e');
} finally {
isSpeaking.value = false;
}
}
/// 停止播放
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');
}
}
/// 切换启用状态
void toggleEnabled() {
_ttsService.toggleEnabled();
statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用';
}
@override
void onClose() {
// 取消定时器
_playbackCheckTimer?.cancel();
// 停止播放
_ttsService.stop();
super.onClose();
}
}

154
lib/modules/test/controllers/flutter_asr_test_controller.dart

@ -1,154 +0,0 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../data/services/flutter_asr_service.dart';
import '../../../core/utils/logger.dart';
/// Flutter ASR测试控制器,演示如何使用FlutterAsrService
class FlutterAsrTestController extends GetxController {
// 获取ASR服务实例
final FlutterAsrService _asrService = Get.find<FlutterAsrService>();
// 当前ASR状态 - 使用Rx包装以便UI可以响应变化
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value;
// 当前识别的文本 - 使用Rx包装以便UI可以响应变化
final RxString _recognizedText = ''.obs;
String get recognizedText => _recognizedText.value;
// 识别准确度 - 使用Rx包装以便UI可以响应变化
final RxDouble _confidence = 0.0.obs;
double get confidence => _confidence.value;
// 是否正在监听 - 计算属性
bool get isListening => asrState == AsrState.listening;
// 是否可用 - 使用Rx包装以便UI可以响应变化
final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value;
// 可用的语言列表 - 使用Rx包装以便UI可以响应变化
final RxList<dynamic> _locales = <dynamic>[].obs;
List<dynamic> get locales => _locales;
// 当前选择的语言 - 使用Rx包装以便UI可以响应变化
final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value;
// 历史记录 - 使用Rx包装以便UI可以响应变化
final RxList<String> _history = <String>[].obs;
List<String> get history => _history;
// 连续识别模式 - 使用Rx包装以便UI可以响应变化
final RxBool _continuousMode = false.obs;
bool get continuousMode => _continuousMode.value;
set continuousMode(bool value) => _continuousMode.value = value;
// 定时器,用于定期更新状态
late Timer _updateTimer;
// 识别结果订阅
StreamSubscription<String>? _resultSubscription;
@override
void onInit() {
super.onInit();
// 初始化状态
_updateState();
// 设置定时器,每300毫秒更新一次状态,确保UI能够及时反映服务状态变化
_updateTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
_updateState();
// 如果启用了连续模式,且当前不在监听状态,且ASR已初始化,则自动重新开始监听
if (_continuousMode.value &&
_asrState.value != AsrState.listening &&
_asrState.value != AsrState.notInitialized &&
_asrState.value != AsrState.error &&
_isAvailable.value) {
_restartListening();
}
});
// 订阅识别结果流
_resultSubscription = _asrService.recognitionResults.listen(_onFinalResult);
}
/// 更新控制器状态,从服务获取最新状态
void _updateState() {
_asrState.value = _asrService.asrState;
_recognizedText.value = _asrService.lastWords;
_confidence.value = _asrService.confidence;
_isAvailable.value = _asrService.isAvailable;
_locales.assignAll(_asrService.locales);
_currentLocale.value = _asrService.currentLocale;
}
/// 开始语音识别
Future<bool> startListening() async {
return await _asrService.startListening();
}
/// 停止语音识别
Future<void> stopListening() async {
await _asrService.stopListening();
}
/// 取消语音识别
Future<void> cancelListening() async {
await _asrService.cancelListening();
}
/// 切换连续识别模式
void toggleContinuousMode() {
_continuousMode.value = !_continuousMode.value;
Logger.info('连续识别模式: ${_continuousMode.value ? '开启' : '关闭'}');
// 如果开启连续模式且当前未在监听,则开始监听
if (_continuousMode.value && !isListening && _isAvailable.value) {
startListening();
}
}
/// 重新开始监听(用于连续模式)
Future<void> _restartListening() async {
// 添加小延迟,避免过于频繁的重启
await Future.delayed(const Duration(milliseconds: 300));
if (_continuousMode.value && !isListening && _isAvailable.value) {
Logger.info('连续模式: 重新开始监听');
await startListening();
}
}
/// 设置语言
void setLocale(String localeId) {
_asrService.setLocale(localeId);
_currentLocale.value = localeId;
}
/// 处理最终识别结果
void _onFinalResult(String result) {
if (result.isNotEmpty) {
// 添加到历史记录
_history.insert(0, result);
// 限制历史记录数量
if (_history.length > 10) {
_history.removeLast();
}
}
}
/// 清空历史记录
void clearHistory() {
_history.clear();
}
@override
void onClose() {
_updateTimer.cancel();
_resultSubscription?.cancel();
super.onClose();
}
}

163
lib/modules/test/controllers/flutter_tts_test_controller.dart

@ -1,163 +0,0 @@
import 'package:get/get.dart';
import '../../../data/services/flutter_tts_service.dart';
import '../../../core/utils/logger.dart';
import 'dart:async';
/// Flutter TTS测试控制器,演示如何使用FlutterTtsService
class FlutterTtsTestController extends GetxController {
// 获取TTS服务实例
final FlutterTtsService _ttsService = Get.find<FlutterTtsService>();
// 当前TTS状态 - 使用Rx包装以便UI可以响应变化
final Rx<TtsState> _ttsState = TtsState.stopped.obs;
TtsState get ttsState => _ttsState.value;
// 当前正在播放的文本 - 使用Rx包装以便UI可以响应变化
final RxString _currentText = ''.obs;
String get currentText => _currentText.value;
// 是否正在播放 - 计算属性
bool get isPlaying => ttsState == TtsState.playing;
// 队列中的文本 - 使用Rx包装以便UI可以响应变化
final RxList<String> _textQueue = <String>[].obs;
List<String> get textQueue => _textQueue;
// 可用的语音列表 - 使用Rx包装以便UI可以响应变化
final RxList<Map<String, String>> _voices = <Map<String, String>>[].obs;
List<Map<String, String>> get voices => _voices;
// 当前选择的语音 - 使用Rx包装以便UI可以响应变化
final RxString _currentVoice = ''.obs;
String get currentVoice => _currentVoice.value;
// 定时器,用于定期更新状态
late Timer _updateTimer;
@override
void onInit() {
super.onInit();
// 初始化状态
_updateState();
// 设置定时器,每300毫秒更新一次状态,确保UI能够及时反映服务状态变化
_updateTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
_updateState();
});
}
/// 更新控制器状态,从服务获取最新状态
void _updateState() {
// 更新TTS状态
final serviceState = _ttsService.ttsState;
if (_ttsState.value != serviceState) {
_ttsState.value = serviceState;
}
// 更新当前播放文本
final serviceText = _ttsService.currentText;
if (_currentText.value != serviceText) {
_currentText.value = serviceText;
}
// 更新队列
final serviceQueue = _ttsService.textQueue;
if (!listEquals(_textQueue, serviceQueue)) {
_textQueue.value = List.from(serviceQueue);
}
// 更新可用语音列表
final serviceVoices = _ttsService.voices;
if (!listEquals(_voices, serviceVoices)) {
_voices.value = List.from(serviceVoices);
}
// 更新当前选择的语音
final serviceCurrentVoice = _ttsService.currentVoice;
if (_currentVoice.value != serviceCurrentVoice) {
_currentVoice.value = serviceCurrentVoice;
}
}
/// 比较两个列表是否相等
bool listEquals<T>(List<T> a, List<T> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
/// 添加文本到队列并开始播放(如果尚未开始)
Future<void> speak(String text) async {
if (text.isEmpty) {
Logger.warning('文本为空,无法播放');
return;
}
await _ttsService.speak(text);
_updateState();
}
/// 添加多条文本到队列并开始播放(如果尚未开始)
Future<void> speakTexts(List<String> texts) async {
if (texts.isEmpty) {
Logger.warning('文本列表为空,无法播放');
return;
}
await _ttsService.speakTexts(texts);
_updateState();
}
/// 停止播放并清空队列
Future<void> stop() async {
await _ttsService.stop();
_updateState();
}
/// 暂停播放
Future<void> pause() async {
await _ttsService.pause();
_updateState();
}
/// 设置语音
Future<void> setVoice(String voiceName) async {
await _ttsService.setVoice(voiceName);
_updateState();
}
/// 设置语言
Future<void> setLanguage(String language) async {
await _ttsService.setLanguage(language);
}
/// 设置语速
Future<void> setRate(double rate) async {
await _ttsService.setRate(rate);
}
/// 设置音量
Future<void> setVolume(double volume) async {
await _ttsService.setVolume(volume);
}
/// 设置音调
Future<void> setPitch(double pitch) async {
await _ttsService.setPitch(pitch);
}
/// 清空队列并停止播放
Future<void> clearQueue() async {
await _ttsService.clearQueue();
_updateState();
}
@override
void onClose() {
_updateTimer.cancel();
super.onClose();
}
}

218
lib/modules/test/controllers/volcano_asr_test_controller.dart

@ -1,218 +0,0 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../data/services/volcano_asr_api_service.dart';
import '../../../core/utils/logger.dart';
class VolcanoAsrTestController extends GetxController {
// Observable variables
final isListening = false.obs;
final errorMessage = ''.obs;
final recognitionResults = <String>[].obs;
final isContinuous = false.obs;
final connectionStatus = 'Disconnected'.obs;
// 当前识别中的文本
final currentRecognizingText = ''.obs;
// 当前识别的索引
int _currentRecognitionIndex = -1;
// Services
late final VolcanoAsrApiService _asrService;
// Stream subscriptions
StreamSubscription? _recognitionSubscription;
@override
void onInit() {
super.onInit();
// Initialize the ASR service
_asrService = Get.find<VolcanoAsrApiService>();
// Subscribe to recognition stream
_recognitionSubscription = _asrService.recognitionStream.listen((event) {
switch (event.type) {
case RecognitionEventType.started:
// 开始新的识别
if (isContinuous.value) {
// 如果是连续模式,检查是否需要创建新条目
if (_currentRecognitionIndex == -1 || recognitionResults.isEmpty) {
// 如果没有当前识别索引或结果列表为空,添加新条目
recognitionResults.add('');
_currentRecognitionIndex = recognitionResults.length - 1;
}
} else {
// 非连续模式,清空结果并添加新条目
recognitionResults.clear();
recognitionResults.add('');
_currentRecognitionIndex = 0;
}
currentRecognizingText.value = '';
break;
case RecognitionEventType.recognizing:
// 实时识别结果
if (event.text.isNotEmpty) {
currentRecognizingText.value = event.text;
// 确保有一个当前条目可以更新
if (_currentRecognitionIndex == -1 || _currentRecognitionIndex >= recognitionResults.length) {
recognitionResults.add('');
_currentRecognitionIndex = recognitionResults.length - 1;
}
// 更新当前条目
recognitionResults[_currentRecognitionIndex] = event.text;
}
break;
case RecognitionEventType.finalResult:
// 最终识别结果
if (event.text.isNotEmpty) {
// 确保有一个当前条目可以更新
if (_currentRecognitionIndex == -1 || _currentRecognitionIndex >= recognitionResults.length) {
recognitionResults.add(event.text);
_currentRecognitionIndex = recognitionResults.length - 1;
} else {
// 更新当前条目为最终结果
recognitionResults[_currentRecognitionIndex] = event.text;
}
// 收到最终结果后,重置索引,为新句子做准备
_currentRecognitionIndex = -1;
// 清空当前识别文本
currentRecognizingText.value = '';
}
break;
case RecognitionEventType.error:
// 错误处理
if (event.error != null) {
errorMessage.value = event.error!;
// 如果有错误且正在识别,停止识别
if (isListening.value) {
isListening.value = false;
connectionStatus.value = 'Disconnected';
// 停止语音识别,不尝试重启
_asrService.stopRecognition();
}
}
break;
// 注意:处理其他事件类型
default:
// 其他事件类型不做特殊处理
break;
}
});
// Update connection status based on service state
ever(_asrService.isListening, (bool listening) {
isListening.value = listening;
connectionStatus.value = listening ? 'Connected' : 'Disconnected';
});
// Update error message from service
if (_asrService.errorMessage.isNotEmpty) {
errorMessage.value = _asrService.errorMessage;
}
}
@override
void onClose() {
// Cancel all subscriptions
_recognitionSubscription?.cancel();
// Disconnect from the ASR service if still connected
if (_asrService.isConnected) {
stopListening();
}
super.onClose();
}
// Start listening for speech
Future<void> startListening() async {
try {
// Clear previous error message
errorMessage.value = '';
// 如果是新的识别会话
if (!isListening.value) {
currentRecognizingText.value = '';
// 如果不是连续识别模式,则清空结果列表
if (!isContinuous.value) {
recognitionResults.clear();
_currentRecognitionIndex = -1;
}
}
// 使用麦克风录音功能
final success = await _asrService.startRecognition(
sampleRate: 16000,
bitsPerSample: 16,
channels: 1,
format: 'pcm',
codec: 'raw',
enablePunc: true,
);
if (success) {
isListening.value = true;
connectionStatus.value = 'Connected';
} else {
errorMessage.value = _asrService.errorMessage;
Logger.error('Failed to start recognition: ${_asrService.errorMessage}');
}
} catch (e) {
errorMessage.value = e.toString();
Logger.error('Error starting recognition: $e');
}
}
// 重新开始识别(用于连续识别模式)
Future<void> _restartRecognition() async {
// 短暂延迟,确保上一次识别完全结束
await Future.delayed(const Duration(milliseconds: 500));
// 如果仍处于监听状态且是连续识别模式,则开始新的识别
if (isListening.value && isContinuous.value) {
startListening();
}
}
// Stop listening for speech
void stopListening() {
try {
connectionStatus.value = 'Disconnecting...';
// 停止麦克风录音
_asrService.stopRecognition().then((_) {
isListening.value = false;
connectionStatus.value = 'Disconnected';
// 重置当前识别索引
_currentRecognitionIndex = -1;
}).catchError((e) {
errorMessage.value = e.toString();
Logger.error('Error stopping recognition: $e');
});
} catch (e) {
errorMessage.value = e.toString();
Logger.error('Error stopping recognition: $e');
}
}
// Clear recognition results
void clearResults() {
recognitionResults.clear();
currentRecognizingText.value = '';
_currentRecognitionIndex = -1;
}
}

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

@ -1,194 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../core/utils/logger.dart';
class VolcanoTtsTestController extends GetxController {
// 使用 VolcanoTtsService 替代 VolcanoTtsApiService
final VolcanoTtsService _ttsService = Get.put(VolcanoTtsService());
// 可观察状态
final isSpeaking = false.obs;
final errorMessage = ''.obs;
final isLoading = false.obs;
final statusMessage = '未连接'.obs;
// 添加实际音频播放状态
final isAudioPlaying = false.obs;
// 播放状态检查定时器
Timer? _playbackCheckTimer;
// 当前播放的索引
final currentPlayingIndex = RxInt(-1);
// 当前使用的语音 - 使用预定义的值
final currentVoice = 'zh_female_meilinvyou_moon_bigtts'.obs;
// 可用的语音列表
final List<Map<String, String>> availableVoices = [
{'id': 'zh_female_meilinvyou_moon_bigtts', 'name': '魅力女友'},
{'id': 'zh_male_shaonianzixin_moon_bigtts', 'name': '少年梓辛/Brayan'},
{'id': 'zh_male_beijingxiaoye_moon_bigtts', 'name': '北京小爷'},
{'id': 'zh_female_daimengchuanmei_moon_bigtts', 'name': '呆萌川妹'},
{'id': 'zh_female_wanqudashu_moon_bigtts', 'name': '湾区大叔'},
{'id': 'zh_female_sajiaonvyou_moon_bigtts', 'name': '撒娇女友'},
{'id': 'zh_male_shenyeboke_moon_bigtts', 'name': '深夜播客'},
];
// 固定的播报内容
final RxList<String> speechTexts = <String>[
'这是第一条播报测试,请听取合成效果。',
'这是第二条播报测试,火山语音合成支持连续多条播报。',
'这是第三条播报测试,可以测试语音合成的流畅度和连贯性。',
'这是第四条播报测试,感谢您的使用。',
].obs;
@override
void onInit() {
super.onInit();
// 初始化UI状态
isSpeaking.value = false;
isAudioPlaying.value = false;
errorMessage.value = '';
isLoading.value = false;
// 监听TTS播放状态
ever(_ttsService.isPlaying, (bool playing) {
isAudioPlaying.value = playing;
if (!playing && currentPlayingIndex.value >= 0) {
// 如果播放结束,重置当前播放索引
currentPlayingIndex.value = -1;
}
});
// 启动播放状态检查定时器
_startPlaybackCheckTimer();
}
/// 更新UI状态
void updateUIState() {
// 强制更新UI
update();
}
/// 播放单条文本
Future<void> playSingle(int index) async {
if (index < 0 || index >= speechTexts.length) {
return;
}
try {
isSpeaking.value = true;
isAudioPlaying.value = true;
errorMessage.value = '';
currentPlayingIndex.value = index;
statusMessage.value = '正在合成...';
final text = speechTexts[index];
// 播放文本
await _ttsService.speak(text);
statusMessage.value = '合成完成';
} catch (e) {
errorMessage.value = '播放出错';
statusMessage.value = '错误';
Logger.error('播放出错: $e');
} finally {
isSpeaking.value = false;
if (!_ttsService.isPlaying.value) {
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
}
}
}
// 启动播放状态检查定时器
void _startPlaybackCheckTimer() {
_playbackCheckTimer?.cancel();
_playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
// 更新实际播放状态
final actuallyPlaying = _ttsService.isPlaying.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 = true;
errorMessage.value = '';
statusMessage.value = '开始连续播放';
// 连接所有文本
final allText = speechTexts.join('');
// 播放所有文本
await _ttsService.speak(allText);
statusMessage.value = '连续播放完成';
} catch (e) {
errorMessage.value = '连续播放出错';
statusMessage.value = '错误';
Logger.error('连续播放出错: $e');
} finally {
isSpeaking.value = false;
}
}
/// 停止播放
Future<void> stopSpeaking() async {
try {
await _ttsService.stop();
isSpeaking.value = false;
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
statusMessage.value = '已停止';
} catch (e) {
errorMessage.value = '停止播放出错';
Logger.error('停止播放出错: $e');
}
}
/// 切换语音
void changeVoice(String voiceId) {
currentVoice.value = voiceId;
// VolcanoTtsService 不需要显式设置发音人,在 speak 时会使用环境变量中的默认发音人
}
/// 切换启用状态
void toggleEnabled() {
_ttsService.isEnabled.toggle();
statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用';
}
@override
void onClose() {
// 取消定时器
_playbackCheckTimer?.cancel();
// 停止播放
_ttsService.stop();
super.onClose();
}
}

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

@ -1,173 +0,0 @@
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);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Azure语音识别测试'),
actions: [
_buildLanguageDropdown(),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusCard(),
const SizedBox(height: 16),
_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: 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: 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,
),
),
)),
),
],
),
),
),
);
}
// 构建控制按钮
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 '未知';
}
}
}

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

@ -1,303 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/azure_tts_test_controller.dart';
class AzureTtsTestView extends GetView<AzureTtsTestController> {
const AzureTtsTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 强制更新控制器状态
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在下一帧渲染完成后调用
controller.updateUIState();
});
return Scaffold(
appBar: AppBar(
title: const Text('Azure语音合成测试'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 语音选择
_buildVoiceSelectionCard(),
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),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() {
return ElevatedButton.icon(
onPressed: controller.isAudioPlaying.value || controller.isLoading.value
? null
: () => controller.speakContinuousTexts(),
icon: controller.isAudioPlaying.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2)
)
: const Icon(Icons.playlist_play),
label: Text(controller.isAudioPlaying.value ? '播放中...' : '全部播放'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
);
}),
Obx(() => ElevatedButton.icon(
onPressed: controller.isAudioPlaying.value
? () => controller.stopSpeaking()
: null,
icon: const Icon(Icons.stop),
label: const Text('停止'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
)),
],
),
],
),
),
),
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),
// 生成可点击的播报内容
Obx(() => ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.speechTexts.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final isPlaying = controller.isAudioPlaying.value &&
controller.currentPlayingIndex.value == index;
return ListTile(
title: Text(
controller.speechTexts[index],
style: TextStyle(
fontWeight: isPlaying ? FontWeight.bold : FontWeight.normal,
color: isPlaying ? Colors.blue : null,
),
),
leading: CircleAvatar(
backgroundColor: isPlaying ? Colors.blue : Colors.grey.shade200,
radius: 18,
child: Icon(
isPlaying ? Icons.volume_up : Icons.play_arrow,
color: isPlaying ? Colors.white : Colors.black,
size: 20,
),
),
trailing: isPlaying
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
onTap: controller.isAudioPlaying.value || controller.isLoading.value
? null
: () => controller.playSingle(index),
enabled: !(controller.isAudioPlaying.value || controller.isLoading.value),
tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: isPlaying
? const BorderSide(color: Colors.blue, width: 1)
: BorderSide.none,
),
);
},
)),
],
),
),
),
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.statusMessage.value,
style: TextStyle(
color: controller.isLoading.value
? Colors.orange
: controller.isAudioPlaying.value
? Colors.green
: Colors.grey,
fontWeight: FontWeight.bold,
),
)),
const SizedBox(height: 8),
Obx(() => controller.errorMessage.value.isNotEmpty
? Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.error_outline, color: Colors.red, size: 16),
SizedBox(width: 8),
Text(
'错误',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 4),
Text(
controller.errorMessage.value,
style: TextStyle(color: Colors.red.shade800),
),
],
),
)
: const SizedBox.shrink()),
],
),
),
),
],
),
),
),
);
}
// 构建语音选择卡片
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,
);
},
),
],
);
}),
],
),
),
);
}
}

476
lib/modules/test/views/flutter_asr_test_view.dart

@ -1,476 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/flutter_asr_test_controller.dart';
import '../../../data/services/flutter_asr_service.dart';
/// Flutter ASR测试视图,演示如何使用FlutterAsrService
class FlutterAsrTestView extends GetView<FlutterAsrTestController> {
const FlutterAsrTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter ASR Test'),
centerTitle: true,
),
body: SingleChildScrollView(
child: 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(
'Speech Recognition Controls',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 添加连续识别模式开关
Obx(() {
final isContinuous = controller.continuousMode;
return SwitchListTile(
title: const Text('连续识别模式'),
subtitle: Text(
isContinuous
? '已开启:识别将自动连续进行'
: '已关闭:识别完成后需手动重新开始',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
value: isContinuous,
onChanged: controller.isAvailable
? (value) => controller.toggleContinuousMode()
: null,
activeColor: Colors.blue,
dense: true,
);
}),
const Divider(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() {
final isListening = controller.isListening;
return ElevatedButton.icon(
onPressed: isListening || !controller.isAvailable
? null
: () => controller.startListening(),
icon: isListening
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.mic),
label: Text(isListening ? 'Listening...' : 'Start Listening'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
);
}),
Obx(() {
final isActive = controller.isListening;
return ElevatedButton.icon(
onPressed: isActive
? () => controller.stopListening()
: null,
icon: const Icon(Icons.stop),
label: const Text('Stop'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
);
}),
],
),
],
),
),
),
const SizedBox(height: 16),
// 语言选择卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Language Selection',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Obx(() {
final locales = controller.locales;
final currentLocale = controller.currentLocale;
if (locales.isEmpty) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'No languages available. Please check ASR settings on your device.',
style: TextStyle(
color: Colors.red,
fontStyle: FontStyle.italic,
),
),
);
}
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Select Language',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
value: currentLocale.isNotEmpty ? currentLocale : null,
items: locales.map((locale) {
final name = locale.name ?? 'Unknown';
final id = locale.localeId ?? '';
return DropdownMenuItem<String>(
value: id,
child: Text('$name (${id.isNotEmpty ? id : 'Unknown'})'),
);
}).toList(),
onChanged: controller.isListening
? null
: (value) {
if (value != null) {
controller.setLocale(value);
}
},
isExpanded: true,
hint: const Text('Select a language'),
);
}),
],
),
),
),
const SizedBox(height: 16),
// 当前识别结果卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Current Recognition',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Obx(() {
final text = controller.recognizedText;
final confidence = controller.confidence;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
width: double.infinity,
child: Text(
text.isEmpty ? 'Speak to see recognition results here' : text,
style: TextStyle(
fontSize: 16,
fontStyle: text.isEmpty ? FontStyle.italic : FontStyle.normal,
color: text.isEmpty ? Colors.grey : Colors.black,
),
),
),
if (confidence > 0)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Confidence: ${(confidence * 100).toStringAsFixed(1)}%',
style: TextStyle(
color: _getConfidenceColor(confidence),
fontWeight: FontWeight.bold,
),
),
),
],
);
}),
],
),
),
),
const SizedBox(height: 16),
// 历史记录卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Recognition History',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Obx(() {
final hasHistory = controller.history.isNotEmpty;
return TextButton.icon(
onPressed: hasHistory ? () => controller.clearHistory() : null,
icon: const Icon(Icons.clear_all, size: 18),
label: const Text('Clear'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
);
}),
],
),
const SizedBox(height: 8),
Obx(() {
final history = controller.history;
if (history.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text(
'No recognition history yet',
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey,
),
),
),
);
}
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: history.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final text = history[index];
return ListTile(
title: Text(text),
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
radius: 18,
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
dense: true,
);
},
);
}),
],
),
),
),
const SizedBox(height: 16),
// 状态显示卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Obx(() {
final state = controller.asrState;
final stateText = _getAsrStateText(state);
final stateColor = _getAsrStateColor(state);
final isContinuous = controller.continuousMode;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: stateColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: stateColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
_getAsrStateIcon(state),
color: stateColor,
),
const SizedBox(width: 8),
Text(
'Current Status: $stateText',
style: TextStyle(
color: stateColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
if (isContinuous)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Row(
children: const [
Icon(
Icons.repeat,
color: Colors.blue,
size: 16,
),
SizedBox(width: 8),
Text(
'连续识别模式已开启',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
),
],
);
}),
],
),
),
),
],
),
),
),
);
}
// 获取ASR状态文本
String _getAsrStateText(AsrState state) {
switch (state) {
case AsrState.notInitialized:
return 'Not Initialized';
case AsrState.initialized:
return 'Initialized';
case AsrState.listening:
return 'Listening';
case AsrState.notListening:
return 'Not Listening';
case AsrState.error:
return 'Error';
default:
return 'Unknown';
}
}
// 获取ASR状态颜色
Color _getAsrStateColor(AsrState state) {
switch (state) {
case AsrState.notInitialized:
return Colors.grey;
case AsrState.initialized:
return Colors.blue;
case AsrState.listening:
return Colors.green;
case AsrState.notListening:
return Colors.orange;
case AsrState.error:
return Colors.red;
default:
return Colors.grey;
}
}
// 获取ASR状态图标
IconData _getAsrStateIcon(AsrState state) {
switch (state) {
case AsrState.notInitialized:
return Icons.hourglass_empty;
case AsrState.initialized:
return Icons.check_circle;
case AsrState.listening:
return Icons.mic;
case AsrState.notListening:
return Icons.mic_off;
case AsrState.error:
return Icons.error;
default:
return Icons.help_outline;
}
}
// 获取置信度颜色
Color _getConfidenceColor(double confidence) {
if (confidence >= 0.8) {
return Colors.green;
} else if (confidence >= 0.6) {
return Colors.orange;
} else {
return Colors.red;
}
}
}

334
lib/modules/test/views/flutter_tts_test_view.dart

@ -1,334 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/flutter_tts_test_controller.dart';
import '../../../data/services/flutter_tts_service.dart';
/// Flutter TTS测试视图,演示如何使用FlutterTtsService
class FlutterTtsTestView extends GetView<FlutterTtsTestController> {
const FlutterTtsTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter TTS Test'),
centerTitle: true,
),
body: SingleChildScrollView(
child: 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(
'Playback Controls',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() {
final isPlaying = controller.isPlaying;
return ElevatedButton.icon(
onPressed: isPlaying
? null
: () => _playAllTexts(),
icon: isPlaying
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.playlist_play),
label: Text(isPlaying ? 'Playing...' : 'Play All'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
);
}),
Obx(() {
final isActive = controller.ttsState != TtsState.stopped;
return ElevatedButton.icon(
onPressed: isActive
? () => controller.stop()
: null,
icon: const Icon(Icons.stop),
label: const Text('Stop'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
);
}),
],
),
],
),
),
),
const SizedBox(height: 16),
// 语音选择卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Voice Selection',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Obx(() {
final voices = controller.voices;
final currentVoice = controller.currentVoice;
if (voices.isEmpty) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'No voices available. Please check TTS settings on your device.',
style: TextStyle(
color: Colors.red,
fontStyle: FontStyle.italic,
),
),
);
}
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Select Voice',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
value: currentVoice.isNotEmpty ? currentVoice : null,
items: voices.map((voice) {
final name = voice['name'] ?? 'Unknown';
final locale = voice['locale'] ?? '';
return DropdownMenuItem<String>(
value: name,
child: Text('$name ${locale.isNotEmpty ? '($locale)' : ''}'),
);
}).toList(),
onChanged: controller.isPlaying
? null
: (value) {
if (value != null) {
controller.setVoice(value);
}
},
isExpanded: true,
hint: const Text('Select a voice'),
);
}),
],
),
),
),
const SizedBox(height: 16),
// 播报内容列表卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Sample Texts',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 预设的播报内容
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _getSampleTexts().length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final text = _getSampleTexts()[index];
return Obx(() {
final isPlaying = controller.isPlaying;
final isCurrentPlaying = controller.isPlaying &&
controller.currentText == text;
return ListTile(
title: Text(
text,
style: TextStyle(
fontWeight: isCurrentPlaying ? FontWeight.bold : FontWeight.normal,
color: isCurrentPlaying ? Colors.blue : null,
),
),
leading: CircleAvatar(
backgroundColor: isCurrentPlaying
? Colors.blue
: Colors.blue.shade100,
radius: 18,
child: Icon(
isCurrentPlaying ? Icons.volume_up : Icons.play_arrow,
color: isCurrentPlaying ? Colors.white : Colors.blue,
size: 20,
),
),
onTap: isPlaying
? null
: () => controller.speak(text),
enabled: !isPlaying,
tileColor: isCurrentPlaying ? Colors.blue.withOpacity(0.1) : null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: isCurrentPlaying
? const BorderSide(color: Colors.blue, width: 1)
: BorderSide.none,
),
);
});
},
),
],
),
),
),
const SizedBox(height: 16),
// 状态显示卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Obx(() {
final state = controller.ttsState;
final stateText = _getTtsStateText(state);
final stateColor = _getTtsStateColor(state);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: stateColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: stateColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
_getTtsStateIcon(state),
color: stateColor,
),
const SizedBox(width: 8),
Text(
'Current Status: $stateText',
style: TextStyle(
color: stateColor,
fontWeight: FontWeight.bold,
),
),
],
),
);
}),
],
),
),
),
],
),
),
),
);
}
// 连续播放所有文本
void _playAllTexts() {
// 使用控制器的连续播放功能播放所有文本
controller.speakTexts(_getSampleTexts());
}
// 获取TTS状态文本
String _getTtsStateText(TtsState state) {
switch (state) {
case TtsState.playing:
return 'Playing';
case TtsState.paused:
return 'Paused';
case TtsState.stopped:
return 'Stopped';
default:
return 'Unknown';
}
}
// 获取TTS状态颜色
Color _getTtsStateColor(TtsState state) {
switch (state) {
case TtsState.playing:
return Colors.green;
case TtsState.paused:
return Colors.orange;
case TtsState.stopped:
return Colors.grey;
default:
return Colors.grey;
}
}
// 获取TTS状态图标
IconData _getTtsStateIcon(TtsState state) {
switch (state) {
case TtsState.playing:
return Icons.volume_up;
case TtsState.paused:
return Icons.pause;
case TtsState.stopped:
return Icons.stop;
default:
return Icons.help_outline;
}
}
// 获取示例文本
List<String> _getSampleTexts() {
return [
'This is a Flutter TTS test. Please listen to the synthesis effect.',
'这是Flutter TTS测试,请听取合成效果。',
'Flutter TTS supports various voices and parameter adjustments.',
'Flutter TTS支持多种语音和参数调节,可以根据需要进行设置。',
];
}
}

234
lib/modules/test/views/volcano_asr_test_view.dart

@ -1,234 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/volcano_asr_test_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class VolcanoAsrTestView extends GetView<VolcanoAsrTestController> {
const VolcanoAsrTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('火山语音识别测试'),
centerTitle: true,
),
body: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 识别模式选择
Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'识别模式',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
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;
}
},
),
),
],
)),
],
),
),
),
SizedBox(height: 16.h),
// 录音控制
Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'录音控制',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16.h),
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: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 12.h,
),
),
)
: ElevatedButton.icon(
onPressed: () => controller.startListening(),
icon: const Icon(Icons.mic),
label: const Text('开始录音'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 12.h,
),
),
),
),
),
],
),
),
),
SizedBox(height: 16.h),
// 识别结果
Expanded(
child: Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'识别结果',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => controller.clearResults(),
tooltip: '清空结果',
),
],
),
SizedBox(height: 8.h),
Expanded(
child: Container(
padding: EdgeInsets.all(8.0.r),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4.0.r),
),
child: Obx(() => ListView.builder(
itemCount: controller.recognitionResults.length,
itemBuilder: (context, index) {
final result = controller.recognitionResults[index];
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.0.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${index + 1}. ',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Expanded(
child: Text(result),
),
],
),
);
},
)),
),
),
],
),
),
),
),
SizedBox(height: 16.h),
// 状态显示
Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'状态',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
Obx(() => Text(
controller.connectionStatus.value,
style: TextStyle(
color: controller.connectionStatus.value == 'Connected'
? Colors.green
: (controller.connectionStatus.value == 'Disconnecting...'
? Colors.orange
: Colors.grey),
fontWeight: FontWeight.bold,
),
)),
SizedBox(height: 8.h),
Obx(() => controller.errorMessage.value.isNotEmpty
? Text(
'错误: ${controller.errorMessage.value}',
style: TextStyle(
color: Colors.red,
fontSize: 14.sp,
),
)
: const SizedBox.shrink()),
],
),
),
),
],
),
),
);
}
}

224
lib/modules/test/views/volcano_tts_test_view.dart

@ -1,224 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/volcano_tts_test_controller.dart';
class VolcanoTtsTestView extends GetView<VolcanoTtsTestController> {
const VolcanoTtsTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 强制更新控制器状态
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在下一帧渲染完成后调用
controller.updateUIState();
});
return Scaffold(
appBar: AppBar(
title: const Text('火山语音合成测试'),
centerTitle: true,
),
body: SingleChildScrollView(
child: 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: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() {
return ElevatedButton.icon(
onPressed: controller.isAudioPlaying.value || controller.isLoading.value
? null
: () => controller.speakContinuousTexts(),
icon: controller.isAudioPlaying.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2)
)
: const Icon(Icons.playlist_play),
label: Text(controller.isAudioPlaying.value ? '播放中...' : '全部播放'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
);
}),
Obx(() => ElevatedButton.icon(
onPressed: controller.isAudioPlaying.value
? () => controller.stopSpeaking()
: null,
icon: const Icon(Icons.stop),
label: const Text('停止'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
)),
],
),
],
),
),
),
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),
// 生成5条可点击的播报内容
Obx(() => ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.speechTexts.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final isPlaying = controller.isAudioPlaying.value &&
controller.currentPlayingIndex.value == index;
return ListTile(
title: Text(
controller.speechTexts[index],
style: TextStyle(
fontWeight: isPlaying ? FontWeight.bold : FontWeight.normal,
color: isPlaying ? Colors.blue : null,
),
),
leading: CircleAvatar(
backgroundColor: isPlaying ? Colors.blue : Colors.grey.shade200,
radius: 18,
child: Icon(
isPlaying ? Icons.volume_up : Icons.play_arrow,
color: isPlaying ? Colors.white : Colors.black,
size: 20,
),
),
trailing: isPlaying
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
onTap: controller.isAudioPlaying.value || controller.isLoading.value
? null
: () => controller.playSingle(index),
enabled: !(controller.isAudioPlaying.value || controller.isLoading.value),
tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: isPlaying
? const BorderSide(color: Colors.blue, width: 1)
: BorderSide.none,
),
);
},
)),
],
),
),
),
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.statusMessage.value,
style: TextStyle(
color: controller.isLoading.value
? Colors.orange
: controller.isAudioPlaying.value
? Colors.green
: Colors.grey,
fontWeight: FontWeight.bold,
),
)),
const SizedBox(height: 8),
Obx(() => controller.errorMessage.value.isNotEmpty
? Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.red.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.error_outline, color: Colors.red, size: 16),
SizedBox(width: 8),
Text(
'错误',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 4),
Text(
controller.errorMessage.value,
style: TextStyle(color: Colors.red.shade800),
),
],
),
)
: const SizedBox.shrink()),
],
),
),
),
],
),
),
),
);
}
}

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

@ -1,7 +1,9 @@
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/speech_impl/azure_asr_service.dart';
import '../../../data/services/speech_impl/azure_tts_service.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/language_manager.dart';
@ -9,8 +11,8 @@ class TranslationBinding implements Bindings {
@override
void dependencies() {
Get.put(LanguageManager(), permanent: true);
Get.put(AzureAsrService(), permanent: true);
Get.put(AzureTtsService(), permanent: true);
Get.lazyPut<AsrService>(() => AzureAsrService(), fenix: true);
Get.lazyPut<TtsService>(() => AzureTtsService(), fenix: true);
Get.put(VolcanoTranslationService(), permanent: true);
Get.put(TranslationController());
}

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

@ -4,16 +4,16 @@ import 'dart:async';
import 'dart:convert';
import 'package:get_storage/get_storage.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/tts_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/asr_service.dart';
class TranslationController extends GetxController {
// 服务
final AzureAsrService _asrService = Get.find<AzureAsrService>();
final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
final TtsService _ttsService = Get.find<TtsService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>();
@ -377,7 +377,7 @@ class TranslationController extends GetxController {
final voiceName = _languageManager.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural';
await _ttsService.setVoice(voiceName);
await _ttsService.speak(text);
await _ttsService.speakOnce(text);
} catch (e) {
Logger.error('播放翻译失败: $e');
}
@ -404,7 +404,7 @@ class TranslationController extends GetxController {
try {
final voiceName = _languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ?? 'en-US-AriaNeural';
await _ttsService.setVoice(voiceName);
await _ttsService.speak(item.translatedText);
await _ttsService.speakOnce(item.translatedText);
} catch (e) {
Logger.error('播放翻译项失败: $e');
}
@ -420,7 +420,7 @@ class TranslationController extends GetxController {
: _languageManager.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ?? 'en-US-AriaNeural';
await _ttsService.setVoice(voiceName);
await _ttsService.speak(text);
await _ttsService.speakOnce(text);
} catch (e) {
Logger.error('播放文本失败: $e');
}

53
lib/routes/app_pages.dart

@ -7,17 +7,7 @@ import '../modules/profile/views/profile_view.dart';
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_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/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';
import '../modules/translation/bindings/translation_binding.dart';
import '../modules/translation/views/translation_history_view.dart';
@ -74,46 +64,7 @@ abstract class AppPages {
page: () => const ProfileView(),
binding: ProfileBinding(),
),
GetPage(
name: Routes.speechDemo,
page: () => const VolcanoAsrTestView(),
binding: VolcanoAsrTestBinding(),
),
GetPage(
name: Routes.TTS_TEST,
page: () => VolcanoTtsTestView(),
binding: VolcanoTtsTestBinding(),
),
GetPage(
name: Routes.ASR_TEST,
page: () => const AzureAsrTestView(),
binding: AzureAsrTestBinding(),
),
GetPage(
name: Routes.AZURE_ASR_TEST,
page: () => const AzureAsrTestView(),
binding: AzureAsrTestBinding(),
),
GetPage(
name: Routes.AZURE_TTS_TEST,
page: () => const AzureTtsTestView(),
binding: AzureTtsTestBinding(),
),
GetPage(
name: Routes.VOLCANO_ASR_TEST,
page: () => const VolcanoAsrTestView(),
binding: VolcanoAsrTestBinding(),
),
GetPage(
name: Routes.FLUTTER_TTS_TEST,
page: () => const FlutterTtsTestView(),
binding: FlutterTtsTestBinding(),
),
GetPage(
name: Routes.FLUTTER_ASR_TEST,
page: () => const FlutterAsrTestView(),
binding: FlutterAsrTestBinding(),
),
GetPage(
name: Routes.translation,
page: () => const TranslationView(),

1
pubspec.yaml

@ -47,6 +47,7 @@ dependencies:
font_awesome_flutter: ^10.8.0
lottie: ^3.3.1
app_settings: 5.2.0
speech_to_text: ^7.0.0
dev_dependencies:
flutter_test:

Loading…
Cancel
Save