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.
484 lines
13 KiB
484 lines
13 KiB
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.toString()}');
|
|
}
|
|
}
|
|
|
|
@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.toString()}');
|
|
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.toString()}');
|
|
|
|
// 添加默认语音
|
|
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.toString()}');
|
|
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.toString()}');
|
|
|
|
// 发送错误事件
|
|
_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.toString()}');
|
|
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.toString()}');
|
|
|
|
// 发送错误事件
|
|
_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.toString()}');
|
|
|
|
// 发送错误事件
|
|
_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.toString()}');
|
|
|
|
// 发送错误事件
|
|
_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.toString()}');
|
|
|
|
// 即使出错也尝试强制重置状态
|
|
_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.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 处理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.toString()}');
|
|
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,
|
|
});
|
|
}
|