import 'dart:async'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../../core/utils/logger.dart'; /// 微软 Text-to-Speech 服务异常 class AzureTtsException implements Exception { final String message; AzureTtsException(this.message); @override String toString() => message; } /// 微软 Text-to-Speech 服务 /// /// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, /// 提供文本转语音功能。 class AzureTtsService extends GetxService { static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_tts'); bool _isInitialized = false; late final String _subscriptionKey; late final String _serviceRegion; // 当前设置 String _currentVoice = 'zh-CN-XiaoxiaoNeural'; int _currentRate = 0; int _currentPitch = 0; int _currentVolume = 100; // 语音合成队列 final List<_SpeechItem> _textQueue = []; bool _isProcessingQueue = false; bool _isSpeaking = false; // 流式输出缓冲区 String _streamBuffer = ''; // 可观察状态 final isEnabled = true.obs; final isSpeaking = false.obs; String get currentVoice => _currentVoice; AzureTtsService() { _loadConfig(); } /// 从环境变量加载配置 void _loadConfig() { _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; } /// 初始化TTS服务 Future initialize({String language = 'zh-CN'}) async { if (_isInitialized) return true; try { if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { Logger.error('未找到Azure语音服务配置'); return false; } final result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey, 'serviceRegion': _serviceRegion, 'language': language, }); _isInitialized = result; return result; } catch (e) { Logger.error('初始化失败: $e'); return false; } } /// 设置语音 Future setVoice(String voiceName) async { if (!_isInitialized) await initialize(); try { final result = await _channel.invokeMethod('setVoice', { 'voiceName': voiceName, }); if (result) _currentVoice = voiceName; return result; } catch (e) { Logger.error('设置语音失败: $e'); return false; } } /// 设置语音参数 Future 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 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 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 speak(String text, {int? rate, int? pitch, int? volume}) async { if (!isEnabled.value || text.isEmpty) return false; _textQueue.add(_SpeechItem( text: text, rate: rate ?? _currentRate, pitch: pitch ?? _currentPitch, volume: volume ?? _currentVolume, )); if (!_isProcessingQueue) _processQueue(); return true; } /// 处理AI流式输出文本 /// /// 将流式输出的文本片段添加到缓冲区,并在检测到完整句子时将其送入播放队列 Future processStreamText(String textChunk) async { if (!isEnabled.value || textChunk.isEmpty) return false; // 添加文本到缓冲区 _streamBuffer += textChunk; // 如果缓冲区为空,直接返回 if (_streamBuffer.isEmpty) return true; // 使用正则表达式匹配句子,包括结束符号 // 匹配任意字符,直到遇到句子结束符号 final sentenceRegex = RegExp( r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])' ); bool hasProcessed = false; // 查找所有完整句子 final matches = sentenceRegex.allMatches(_streamBuffer); final List 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 flushStreamBuffer() async { if (!isEnabled.value || _streamBuffer.isEmpty) return false; // 先尝试使用processStreamText处理可能的完整句子 await processStreamText(''); // 如果缓冲区仍有剩余文本,将其添加到播放队列 if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { _textQueue.add(_SpeechItem( text: _streamBuffer, rate: _currentRate, pitch: _currentPitch, volume: _currentVolume, )); // 清空缓冲区 _streamBuffer = ''; // 如果队列未在处理中,开始处理队列 if (!_isProcessingQueue) { _processQueue(); } } return true; } /// 连续播放多段文本 Future speakMultiple(List texts, {int? rate, int? pitch, int? volume}) async { if (!isEnabled.value || texts.isEmpty) return false; for (final text in texts) { if (text.isNotEmpty) { _textQueue.add(_SpeechItem( text: text, rate: rate ?? _currentRate, pitch: pitch ?? _currentPitch, volume: volume ?? _currentVolume, )); } } if (!_isProcessingQueue) _processQueue(); return true; } /// 处理语音合成队列 Future _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 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 ''' $text '''; } /// 切换启用状态 void toggleEnabled() { isEnabled.toggle(); if (!isEnabled.value) stop(); } /// 释放资源 Future 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, }); }