import 'dart:async'; import 'dart:io'; 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'; /// 微软 Text-to-Speech 服务 /// /// 该服务通过平台通道与原生 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; // 支持的语音列表 final List _supportedVoices = [ 'zh-CN-XiaoxiaoNeural', 'zh-CN-YunxiNeural', 'zh-CN-YunjianNeural', 'en-US-JennyNeural', 'en-US-GuyNeural' ]; // 语音合成队列 final List<_SpeechItem> _textQueue = []; bool _isProcessingQueue = false; // 可观察状态 final isEnabled = true.obs; // 流式文本缓冲区 String _streamBuffer = ''; /// 缓存的播放状态 bool _speakingCache = false; DateTime _lastSpeakingCheck = DateTime.now(); @override String get currentVoice => _currentVoice; @override bool get isSpeaking { // 如果未初始化,直接返回false if (!_isInitialized) return false; // 如果距离上次检查不到50ms,则使用缓存值 if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) { return _speakingCache; } // 否则异步更新缓存(但仍返回当前缓存值) _checkSpeakingStatus(); return _speakingCache; } @override Stream get onEvent => const Stream.empty(); @override List get supportedVoices => _supportedVoices; AzureTtsService() { _loadConfig(); } /// 从环境变量加载配置 void _loadConfig() { _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; } @override Future initialize({ List? 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; return result; } else { // 使用默认语言初始化 final result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey, 'region': _serviceRegion, 'language': 'zh-CN', }); _isInitialized = result; return result; } } catch (e) { Logger.error('初始化失败: $e'); return false; } } @override Future 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 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; } } @override Future speakOnce(String text) async { if (!_isInitialized) await initialize(); if (!isEnabled.value || text.isEmpty) return false; try { // 主动设置缓存状态为true _speakingCache = true; final result = await _channel.invokeMethod('speakText', {'text': text}); // 调用完成后主动检查状态 _checkSpeakingStatus(); return result == "OK"; } catch (e) { // 发生错误时重置状态 _speakingCache = false; Logger.error('语音合成失败: $e'); return false; } } /// 播放SSML Future speakSsmlOnce(String ssml) async { if (!_isInitialized) await initialize(); if (!isEnabled.value || ssml.isEmpty) return false; try { // 主动设置缓存状态为true _speakingCache = true; final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); // 调用完成后主动检查状态 _checkSpeakingStatus(); return result == "OK"; } catch (e) { // 发生错误时重置状态 _speakingCache = false; Logger.error('SSML语音合成失败: $e'); return false; } } /// 等待语音播放完成 Future waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async { // 首先检查当前状态 _checkSpeakingStatus(); if (!_speakingCache) return; final stopwatch = Stopwatch()..start(); while (_speakingCache && stopwatch.elapsed < timeout) { // 短暂等待后再次检查 await Future.delayed(const Duration(milliseconds: 100)); // 检查状态 await _channel.invokeMethod('isSpeaking').then((result) { if (result != null) { _speakingCache = result; _lastSpeakingCheck = DateTime.now(); } }); } } @override Future 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 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'); return false; } } @override Future 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}条'); // 等待队列处理完成 await _checkSpeakingStatus(); while (_isProcessingQueue || _speakingCache) { await Future.delayed(const Duration(milliseconds: 100)); await _checkSpeakingStatus(); } Logger.info('TTS队列播放完成'); return true; } catch (e) { Logger.error('刷新TTS流失败: $e'); return false; } } /// 处理语音合成队列 Future _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); // 等待播放完成 await waitForSpeechCompletion(); } } catch (e) { Logger.error('处理语音队列出错: $e'); } finally { _isProcessingQueue = false; } } @override Future stop() async { try { _textQueue.clear(); if (_speakingCache) { final result = await _channel.invokeMethod('stopSpeaking'); // 重置缓存状态 _speakingCache = false; return result; } return true; } catch (e) { Logger.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 ''' $text '''; } /// 切换启用状态 void toggleEnabled() { isEnabled.toggle(); if (!isEnabled.value) stop(); } @override Future dispose() async { if (!_isInitialized) return; try { await stop(); await _channel.invokeMethod('dispose'); _isInitialized = false; } catch (e) { Logger.error('释放资源失败: $e'); } } /// 异步检查并更新播放状态缓存 Future _checkSpeakingStatus() async { _lastSpeakingCheck = DateTime.now(); try { final result = await _channel.invokeMethod('isSpeaking'); if (result != null) { _speakingCache = result; } return _speakingCache; } catch (e) { Logger.error('获取播放状态失败: $e'); return _speakingCache; } } } /// 语音合成项目 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, }); }