You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

477 lines
13 KiB

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<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 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<TtsEvent> get onEvent => const Stream.empty();
@override
List<String> get supportedVoices => _supportedVoices;
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;
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<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> 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<bool> 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<bool> 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<void> 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<bool>('isSpeaking').then((result) {
if (result != null) {
_speakingCache = result;
_lastSpeakingCheck = DateTime.now();
}
});
}
}
@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');
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}条');
// 等待队列处理完成
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<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);
// 等待播放完成
await waitForSpeechCompletion();
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
} finally {
_isProcessingQueue = false;
}
}
@override
Future<bool> 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 '''
<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();
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
await _channel.invokeMethod('dispose');
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
}
}
/// 异步检查并更新播放状态缓存
Future<bool> _checkSpeakingStatus() async {
_lastSpeakingCheck = DateTime.now();
try {
final result = await _channel.invokeMethod<bool>('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,
});
}