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.

430 lines
12 KiB

import 'dart:async';
import 'dart:io';
import '../../../data/models/appconfig.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.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('azure_speech/tts');
static const EventChannel _eventChannel =
EventChannel('azure_speech/tts_events');
// final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
// 当前设置
String _currentVoice = 'zh-CN-XiaoxiaoNeural';
// 语音合成队列
final List<String> _textQueue = [];
bool _isProcessingQueue = false;
// 事件流
final StreamController<TtsEvent> _eventStreamController =
StreamController<TtsEvent>.broadcast();
StreamSubscription? _eventSubscription;
// 可观察状态
final isEnabled = true.obs;
final _isSpeaking = false.obs;
// 流式文本缓冲区
String _streamBuffer = '';
@override
String get currentVoice => _currentVoice;
@override
bool get isSpeaking => _isSpeaking.value;
@override
Stream<TtsEvent> get onEvent => _eventStreamController.stream;
AzureTtsService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
// final _env = _storage.read("ENV") as Map<String, String>;
_subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? '';
_serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? '';
}
/// 设置事件通道
void _setupEventChannel() {
_eventSubscription?.cancel();
_eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) {
if (event is Map) {
_handleTtsEvent(event);
}
}, onError: (error) {
Logger.error('TTS事件流错误: ${error.toString()}');
_eventStreamController.addError(error);
});
}
/// 处理TTS事件
void _handleTtsEvent(Map<dynamic, dynamic> event) {
final String eventType = event['type'] as String? ?? '';
// Logger.debug('收到TTS事件: ${e.toString()}ventType, 数据: ${e.toString()}vent');
switch (eventType) {
case 'synthesis_started':
_isSpeaking.value = true;
_eventStreamController.add(TtsEvent(type: TtsEventType.started));
break;
case 'synthesizing':
// _isSpeaking.value = true;
// // 合成中事件暂时映射到started类型,因为TtsEventType中没有synthesizing类型
// _eventStreamController.add(TtsEvent(type: TtsEventType.started));
break;
case 'synthesis_completed':
_isSpeaking.value = false;
_eventStreamController.add(TtsEvent(type: TtsEventType.completed));
// 如果有队列在处理中,继续处理下一项
if (_isProcessingQueue) {
_processNextInQueue();
}
break;
case 'synthesis_canceled':
_isSpeaking.value = false;
final String reason = event['reason'] as String? ?? '';
_eventStreamController.add(TtsEvent.error('合成取消: $reason'));
// 如果有队列在处理中,继续处理下一项(即使出错)
if (_isProcessingQueue) {
_processNextInQueue();
}
break;
case 'error':
_isSpeaking.value = false;
final String message = event['message'] as String? ?? '未知错误';
Logger.error('TTS错误: $message');
_eventStreamController.add(TtsEvent.error(message));
// 如果有队列在处理中,继续处理下一项(即使出错)
if (_isProcessingQueue) {
_processNextInQueue();
}
break;
}
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
// 检查是否需要重新初始化
if (_isInitialized) {
await dispose();
}
try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
Logger.error('未找到Azure语音服务配置');
return false;
}
// 先设置事件通道,确保在调用initialize方法前已准备好
_setupEventChannel();
// 更新支持的语音列表(如果提供)
final String language = supportedLanguages?.first ?? 'zh-CN';
final result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'region': _serviceRegion,
'language': language,
});
_isInitialized = result;
return result;
} catch (e) {
Logger.error('初始化失败: ${e.toString()}');
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.toString()}');
return false;
}
}
@override
Future<bool> speakOnce(String text) async {
if (!_isInitialized) await initialize();
if (!isEnabled.value || text.isEmpty) return false;
try {
// 开始合成,传递参数
final result = await _channel.invokeMethod('speakText', {
'text': text,
});
return result == true;
} catch (e) {
Logger.error('语音合成失败: ${e.toString()}');
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(sentence);
hasProcessed = true;
}
// 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd);
}
// 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) {
_processQueue();
}
return true;
} catch (e) {
Logger.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(_streamBuffer);
// 清空缓冲区
_streamBuffer = '';
// 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) {
_processQueue();
}
}
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成
final completer = Completer<bool>();
// 监听事件流检查队列是否完成
final subscription = onEvent.listen((event) {
// 当队列为空且不在处理中,且当前非说话状态,完成操作
if (_textQueue.isEmpty &&
!_isProcessingQueue &&
!_isSpeaking.value &&
!completer.isCompleted) {
completer.complete(true);
}
});
// 如果已经处于空闲状态,直接完成
if (_textQueue.isEmpty && !_isProcessingQueue && !_isSpeaking.value) {
subscription.cancel();
return true;
}
// 设置超时
final timeout = Timer(const Duration(seconds: 60), () {
if (!completer.isCompleted) {
Logger.warning('等待TTS队列完成超时');
completer.complete(false);
}
});
// 等待完成
final result = await completer.future;
// 清理
subscription.cancel();
timeout.cancel();
Logger.info('TTS队列播放完成');
return result;
} catch (e) {
Logger.error('刷新TTS流失败: ${e.toString()}');
return false;
}
}
/// 处理语音合成队列
Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true;
// 处理队列中的第一项
await _processNextInQueue();
}
/// 处理队列中的下一项
Future<void> _processNextInQueue() async {
// 检查队列是否为空或已禁用
if (_textQueue.isEmpty || !isEnabled.value) {
_textQueue.clear();
_isProcessingQueue = false;
return;
}
try {
// 获取当前项但暂不从队列中移除
final text = _textQueue.first;
// 开始合成
final success = await speakOnce(text);
// 仅当合成操作失败时才从队列移除并继续处理下一项
// 成功的话会等待合成完成事件后自动处理下一项
if (!success) {
_textQueue.removeAt(0);
await _processNextInQueue();
} else {
// 成功启动合成,从队列中移除当前项
// 注意:下一项的处理将在合成完成事件中触发
_textQueue.removeAt(0);
}
} catch (e) {
Logger.error('处理语音队列出错: ${e.toString()}');
// 出错时移除当前项并尝试继续
if (_textQueue.isNotEmpty) {
_textQueue.removeAt(0);
}
// 如果队列中还有项目,继续处理
if (_textQueue.isNotEmpty) {
await _processNextInQueue();
} else {
_isProcessingQueue = false;
}
}
}
@override
Future<bool> stop() async {
try {
_textQueue.clear();
if (_isSpeaking.value) {
final result = await _channel.invokeMethod('stopSpeaking');
// 重置缓存状态
_isSpeaking.value = false;
_isProcessingQueue = false;
return result;
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: ${e.toString()}');
return false;
}
}
/// 切换启用状态
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) stop();
}
@override
Future<void> dispose() async {
if (!_isInitialized) return;
try {
await stop();
await _eventSubscription?.cancel();
await _channel.invokeMethod('dispose');
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: ${e.toString()}');
}
}
@override
Future<bool> setAudioOutputDevice(int type) async {
try {
// 开始合成,传递参数
final result = await _channel.invokeMethod('setAudioOutputDevice', {
'type': type,
});
return result;
} catch (e) {
Logger.error('音频输出设备类型失败: ${e.toString()}');
return false;
}
}
}