Files
ai_chat_assistant/lib/services/tts_service.dart

610 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:collection';
import 'package:ai_chat_assistant/utils/common_util.dart';
import 'package:ai_chat_core/ai_chat_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
@Deprecated('Use TtsService interface and its implementations instead')
class LocalTtsService {
final FlutterTts _flutterTts = FlutterTts();
bool _isPlaying = false;
// TTS 播放队列 - 存储文本段
final Queue<String> _ttsQueue = Queue<String>();
// TTS 事件回调
VoidCallback? _onStartHandler;
VoidCallback? _onCompletionHandler;
Function(String)? _onErrorHandler;
// 有序播放队列 - 确保播放顺序与文本顺序一致
List<TtsTask> _orderedTasks = []; // 有序任务队列
int _nextTaskIndex = 0; // 下一个要处理的任务索引
bool _isProcessingTasks = false; // 是否正在处理任务
bool _streamCompleted = false; // 流是否完成
LocalTtsService() {
_initializeTts();
}
/// 初始化TTS引擎
Future<void> _initializeTts() async {
try {
// 获取可用的TTS引擎列表
await _printAvailableEngines();
// 推荐并设置最佳引擎
// await setEngine("com.k2fsa.sherpa.onnx.tts.engine");
// 设置TTS参数
await _flutterTts.setLanguage("en-US"); // 默认英文
await _flutterTts.setSpeechRate(0.5); // 语速
await _flutterTts.setVolume(1.0); // 音量
await _flutterTts.setPitch(1.0); // 音调
// 设置TTS事件回调
_flutterTts.setStartHandler(() {
Logger.i('TTS开始播放');
_isPlaying = true;
_onStartHandler?.call();
});
_flutterTts.setCompletionHandler(() {
Logger.i('TTS播放完成');
_isPlaying = false;
_handleTtsPlaybackComplete();
});
_flutterTts.setErrorHandler((msg) {
Logger.e('TTS错误: $msg');
_isPlaying = false;
_onErrorHandler?.call(msg);
_handleTtsPlaybackComplete(); // 错误时也要处理下一个任务
});
Logger.i('TTS引擎初始化完成');
} catch (e) {
Logger.e('TTS引擎初始化失败: $e');
}
}
/// 设置最佳TTS引擎
// 已移除TtsEngineManager相关代码避免未定义错误。
/// 打印可用的TTS引擎
Future<void> _printAvailableEngines() async {
try {
// 获取可用的TTS引擎
dynamic engines = await _flutterTts.getEngines;
Logger.i('可用的TTS引擎: $engines');
// 获取可用的语言
dynamic languages = await _flutterTts.getLanguages;
Logger.i('支持的语言: $languages');
// 获取当前默认引擎
dynamic defaultEngine = await _flutterTts.getDefaultEngine;
Logger.i('当前默认引擎: $defaultEngine');
} catch (e) {
Logger.e('获取TTS引擎信息失败: $e');
}
}
/// 清空所有队列和缓存
void clearAll() {
_ttsQueue.clear();
_orderedTasks.clear();
_nextTaskIndex = 0;
_isProcessingTasks = false;
_streamCompleted = false;
_isPlaying = false; // 重置播放状态
Logger.i('所有TTS队列和缓存已清空');
}
/// 推送文本到TTS队列进行流式处理保证顺序
void pushTextForStreamTTS(String text) {
if (text.trim().isEmpty) {
Logger.i('接收到空字符串跳过推送到TTS队列');
return;
}
String cleanedText = CommonUtil.cleanText(text, true);
if (cleanedText.isEmpty) {
Logger.i('清理后的文本为空跳过推送到TTS队列');
return;
}
// 创建有序任务
int taskIndex = _orderedTasks.length;
TtsTask task = TtsTask(
index: taskIndex,
text: cleanedText,
status: TtsTaskStatus.ready,
);
_orderedTasks.add(task);
Logger.i('添加TTS任务 (索引: $taskIndex): $cleanedText');
// 尝试处理队列中的下一个任务
_processNextReadyTask();
}
/// 处理队列中下一个就绪的任务
void _processNextReadyTask() {
// 如果正在播放,则不处理下一个任务
if (_isPlaying) {
Logger.i('当前正在播放音频,等待播放完成');
return;
}
// 寻找下一个可以播放的任务
while (_nextTaskIndex < _orderedTasks.length) {
TtsTask task = _orderedTasks[_nextTaskIndex];
if (task.status == TtsTaskStatus.ready) {
Logger.i('按顺序播放TTS (索引: ${task.index}): ${task.text}');
_playTts(task.text);
return; // 等待当前音频播放完成
} else {
// 跳过已处理的任务
_nextTaskIndex++;
continue;
}
}
// 检查是否所有任务都已处理完
if (_streamCompleted && _nextTaskIndex >= _orderedTasks.length) {
Logger.i('=== 所有TTS任务播放完成 ===');
_isProcessingTasks = false;
_onCompletionHandler?.call();
}
}
/// 播放TTS
Future<void> _playTts(String text) async {
if (_isPlaying) {
Logger.i('已有TTS正在播放跳过本次播放');
return;
}
try {
Logger.i('开始播放TTS: $text');
// 检测语言并设置
bool isChinese = CommonUtil.containChinese(text);
String targetLanguage = isChinese ? "zh-CN" : "en-US";
// 检查语言是否可用,如果不可用则使用默认语言
bool isLanguageOk = await isLanguageAvailable(targetLanguage);
if (!isLanguageOk) {
Logger.i('语言 $targetLanguage 不可用,使用默认语言');
// 可以在这里设置备用语言或保持当前语言
} else {
await _flutterTts.setLanguage(targetLanguage);
Logger.i('设置TTS语言为: $targetLanguage');
}
// 播放TTS
await _flutterTts.speak(text);
} catch (e) {
Logger.i('播放TTS失败: $e');
// 播放失败,重置状态并继续下一个
_isPlaying = false;
_handleTtsPlaybackComplete();
}
}
/// 处理TTS播放完成
void _handleTtsPlaybackComplete() {
if (!_isPlaying) {
Logger.i('TTS已停止播放处理下一个任务');
// 标记当前任务为已完成
if (_nextTaskIndex < _orderedTasks.length &&
_orderedTasks[_nextTaskIndex].status == TtsTaskStatus.ready) {
_orderedTasks[_nextTaskIndex].status = TtsTaskStatus.completed;
_nextTaskIndex++;
}
// 处理下一个任务
_processNextReadyTask();
}
}
/// 标记流完成
void markSSEStreamCompleted() {
Logger.i('=== 标记SSE流完成 ===');
_streamCompleted = true;
// 尝试完成剩余任务
_processNextReadyTask();
}
/// 停止播放
void stopSSEPlayback() {
Logger.i("停止SSE播放");
// 立即停止TTS播放无论当前是否在播报
_flutterTts.stop();
_isPlaying = false;
clearAll();
}
// 设置事件处理器
void setStartHandler(VoidCallback handler) {
_onStartHandler = handler;
}
void setCompletionHandler(VoidCallback handler) {
_onCompletionHandler = handler;
}
void setErrorHandler(Function(String) handler) {
_onErrorHandler = handler;
}
// 保留原有的播放逻辑用于完整文本处理
Future<void> processCompleteText(String text) async {
if (text.trim().isEmpty) {
Logger.i("文本为空跳过TTS处理");
return;
}
Logger.i("开始处理完整文本TTS原始文本长度: ${text.length}");
// 清理缓存和重置队列
clearAll();
// 确保停止当前播放
await _stopCurrentPlayback();
// 直接使用流式处理
await _processCompleteTextAsStream(text);
}
/// 将完整文本作为流式处理
Future<void> _processCompleteTextAsStream(String text) async {
// 清理markdown和异常字符
String cleanedText = _cleanTextForTts(text);
Logger.i("清理后文本长度: ${cleanedText.length}");
if (cleanedText.trim().isEmpty) {
Logger.i("清理后文本为空跳过TTS");
return;
}
// 分割成句子并逐个处理
List<String> sentences = _splitTextIntoSentences(cleanedText);
Logger.i("=== 文本分段完成,共 ${sentences.length} 个句子 ===");
// 推送每个句子到流式TTS处理
for (int i = 0; i < sentences.length; i++) {
final sentence = sentences[i].trim();
if (sentence.isEmpty) continue;
Logger.i("处理句子 ${i + 1}/${sentences.length}: $sentence");
pushTextForStreamTTS(sentence);
}
// 标记流完成
markSSEStreamCompleted();
}
// 停止当前播放的辅助方法
Future<void> _stopCurrentPlayback() async {
try {
Logger.i("停止当前播放");
if (_isPlaying) {
await _flutterTts.stop();
_isPlaying = false;
Logger.i("TTS.stop() 调用完成");
}
// 短暂延迟确保播放器状态稳定
await Future.delayed(Duration(milliseconds: 300));
Logger.i("播放器状态稳定延迟完成");
} catch (e) {
Logger.i('停止当前播放错误: $e');
}
}
// 保留原有的方法用于兼容性
List<String> _splitTextIntoSentences(String text) {
List<String> result = [];
if (text.length <= 50) {
result.add(text);
return result;
}
// 支持中英文句子分割
List<String> sentenceEnders = [
'', '', '', '', '\n', // 中文
'.', '!', '?', ';' // 英文
];
String remainingText = text;
while (remainingText.isNotEmpty) {
if (remainingText.length <= 50) {
result.add(remainingText.trim());
break;
}
// 在50字以内查找最佳分割点
String candidate = remainingText.substring(0, 50);
int lastSentenceEndIndex = -1;
for (int i = candidate.length - 1; i >= 0; i--) {
if (sentenceEnders.contains(candidate[i])) {
lastSentenceEndIndex = i;
break;
}
}
if (lastSentenceEndIndex != -1) {
// 找到句子结束符,以此为分割点
String chunk = candidate.substring(0, lastSentenceEndIndex + 1).trim();
if (chunk.isNotEmpty) {
result.add(chunk);
}
remainingText =
remainingText.substring(lastSentenceEndIndex + 1).trim();
} else {
// 没找到句子结束符,寻找逗号分割点(中英文逗号)
int lastCommaIndex = -1;
for (int i = candidate.length - 1; i >= 20; i--) {
if (candidate[i] == '' || candidate[i] == ',') {
lastCommaIndex = i;
break;
}
}
if (lastCommaIndex != -1) {
String chunk = candidate.substring(0, lastCommaIndex + 1).trim();
if (chunk.isNotEmpty) {
result.add(chunk);
}
remainingText = remainingText.substring(lastCommaIndex + 1).trim();
} else {
// 没找到合适的分割点强制在50字处分割
result.add(candidate.trim());
remainingText = remainingText.substring(50).trim();
}
}
}
return result.where((chunk) => chunk.trim().isNotEmpty).toList();
}
String _cleanTextForTts(String text) {
String cleanText = text
// 修正markdown粗体格式保留内容
.replaceAllMapped(RegExp(r'\*\*(.*?)\*\*'),
(m) => m.group(1) ?? '') // 粗体:**文字** -> 文字
.replaceAllMapped(
RegExp(r'\*(.*?)\*'), (m) => m.group(1) ?? '') // 斜体:*文字* -> 文字
.replaceAllMapped(
RegExp(r'`([^`]+)`'), (m) => m.group(1) ?? '') // 行内代码:`代码` -> 代码
.replaceAll(RegExp(r'```[^`]*```', multiLine: true), '') // 代码块:完全移除
.replaceAll(RegExp(r'```[\s\S]*?```'), '') // 代码块:完全移除(备用)
.replaceAll(
RegExp(r'^#{1,6}\s+(.*)$', multiLine: true), r'$1') // 标题:# 标题 -> 标题
.replaceAll(
RegExp(r'\[([^\]]+)\]\([^\)]+\)'), r'$1') // 链接:[文字](url) -> 文字
.replaceAll(
RegExp(r'^>\s*(.*)$', multiLine: true), r'$1') // 引用:> 文字 -> 文字
.replaceAll(RegExp(r'^[-*+]\s+(.*)$', multiLine: true),
r'$1') // 无序列表:- 项目 -> 项目
.replaceAll(RegExp(r'^\d+\.\s+(.*)$', multiLine: true),
r'$1') // 有序列表1. 项目 -> 项目
.replaceAll(RegExp(r'^\s*[-*]{3,}\s*$', multiLine: true), '') // 分隔线:移除
.replaceAll(RegExp(r'\n\s*\n'), '\n') // 多个换行替换为单个换行
.replaceAll(RegExp(r'\n'), ' ') // 换行替换为空格
// 移除代码相关的变量和符号(但保留正常文字中的这些字符)
.replaceAll(RegExp(r'\$\d+(?=\s|$|[,。!?;:])'), '') // 移除独立的 $1, $2, $3 等
.replaceAll(RegExp(r'\$\{[^}]*\}'), '') // 移除 ${variable} 格式
.replaceAll(RegExp(r'(?<=\s|^)\$[a-zA-Z_]\w*(?=\s|$|[,。!?;:])'),
'') // 移除独立的 $variable 格式
// 移除HTML相关内容
.replaceAll(RegExp(r'\\[ntr]'), ' ') // 移除转义字符
.replaceAll(RegExp(r'&[a-zA-Z0-9]+;'), ' ') // 移除HTML实体
.replaceAll(RegExp(r'<[^>]*>'), '') // 秼除HTML标签
// 移除明显的代码结构(保留正常文字中的括号)
.replaceAll(RegExp(r'\{[^}]*\}(?=\s|$)'), '') // 移除独立的花括号内容
.replaceAll(
RegExp(r'(?<=\s|^)@[a-zA-Z_]\w*(?=\s|$)'), '') // 移除独立的 @mentions
// 移除纯编程符号行,但保留文字中的标点
.replaceAll(
RegExp(r'^[#%&*+=|\\/<>^~`\s]*$', multiLine: true), '') // 移除纯符号行
.replaceAll(
RegExp(r'(?<=\s)[#%&*+=|\\/<>^~`]+(?=\s)'), ' ') // 移除词间的编程符号
// 移除表情符号
.replaceAll(RegExp(r'[\u{1F600}-\u{1F64F}]', unicode: true), '') // 表情符号
.replaceAll(
RegExp(r'[\u{1F300}-\u{1F5FF}]', unicode: true), '') // 符号和象形文字
.replaceAll(
RegExp(r'[\u{1F680}-\u{1F6FF}]', unicode: true), '') // 运输和地图符号
.replaceAll(RegExp(r'[\u{1F1E0}-\u{1F1FF}]', unicode: true), '') // 旗帜
.replaceAll(RegExp(r'[\u{2600}-\u{26FF}]', unicode: true), '') // 杂项符号
// 移除特殊的非文字字符,但保留基本标点
.replaceAll(
RegExp(r'[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:、""'
'()[\]【】《》〈〉「」『』\-—_\n]'),
' ');
// 清理多余的空格和标点
cleanText = cleanText
.replaceAll(RegExp(r'\s+'), ' ') // 多个空格替换为单个空格
.replaceAll(RegExp(r'[。]{2,}'), '') // 多个句号替换为单个
.replaceAll(RegExp(r'[]{2,}'), '') // 多个逗号替换为单个
.replaceAll(RegExp(r'[]{2,}'), '') // 多个感叹号替换为单个
.replaceAll(RegExp(r'[]{2,}'), '') // 多个问号替换为单个
.replaceAll(RegExp(r'^\s+|\s+$'), '') // 移除首尾空格
.trim();
return cleanText;
}
// 单次语音播放方法
Future<bool> speak(String text) async {
if (text.trim().isEmpty) return false;
try {
await stop();
await processCompleteText(text);
return true;
} catch (e) {
Logger.i('TTS 播放错误: $e');
_onErrorHandler?.call('TTS 播放错误: $e');
return false;
}
}
// 停止播放
Future<void> stop() async {
try {
await _stopCurrentPlayback();
clearAll();
} catch (e) {
Logger.i('停止音频播放错误: $e');
}
}
Future<void> pause() async {
try {
if (_isPlaying) {
await _flutterTts.pause();
}
} catch (e) {
Logger.i('暂停音频播放错误: $e');
}
}
Future<void> resume() async {
try {
// flutter_tts没有resume方法需要重新播放当前文本
// 这里可以根据需要实现
Logger.i('TTS不支持恢复需要重新播放');
} catch (e) {
Logger.i('恢复音频播放错误: $e');
}
}
bool get isPlaying => _isPlaying;
bool get hasQueuedItems => _orderedTasks.isNotEmpty;
int get queueLength => _orderedTasks.length;
// 获取当前播放状态
bool get isCurrentlyPlaying => _isPlaying;
// 获取任务队列状态信息
Map<String, dynamic> get sseQueueStatus => {
'totalTasks': _orderedTasks.length,
'completedTasks': _nextTaskIndex,
'isProcessing': _isProcessingTasks,
'streamCompleted': _streamCompleted,
};
// 获取队列状态信息(保留兼容性)
Map<String, dynamic> get queueStatus => {
'totalUrls': 0,
'currentIndex': 0,
'isQueuePlaying': false,
'isAllSegmentsProcessed': false,
'expectedSegments': 0,
};
// 获取有序任务数量(用于测试)
int getOrderedTasksCount() => _orderedTasks.length;
// 获取下一个任务索引(用于测试)
int getNextTaskIndex() => _nextTaskIndex;
// dispose方法
void dispose() {
clearAll();
// flutter_tts会自动清理资源
}
/// 获取可用的TTS引擎列表
Future<List<dynamic>> getAvailableEngines() async {
try {
dynamic engines = await _flutterTts.getEngines;
return engines ?? [];
} catch (e) {
Logger.i('获取TTS引擎列表失败: $e');
return [];
}
}
/// 设置TTS引擎
Future<bool> setEngine(String engineName) async {
try {
int result = await _flutterTts.setEngine(engineName);
Logger.i('设置TTS引擎: $engineName, 结果: $result');
return result == 1; // 1表示成功
} catch (e) {
Logger.i('设置TTS引擎失败: $e');
return false;
}
}
/// 获取当前默认引擎
Future<String?> getCurrentEngine() async {
try {
dynamic engine = await _flutterTts.getDefaultEngine;
return engine?.toString();
} catch (e) {
Logger.i('获取当前TTS引擎失败: $e');
return null;
}
}
/// 获取支持的语言列表
Future<List<dynamic>> getSupportedLanguages() async {
try {
dynamic languages = await _flutterTts.getLanguages;
return languages ?? [];
} catch (e) {
Logger.i('获取支持的语言列表失败: $e');
return [];
}
}
/// 检查特定语言是否可用
Future<bool> isLanguageAvailable(String language) async {
try {
dynamic result = await _flutterTts.isLanguageAvailable(language);
return result == true;
} catch (e) {
Logger.i('检查语言可用性失败: $e');
return false;
}
}
}
/// TTS任务类
class TtsTask {
final int index;
final String text;
TtsTaskStatus status;
TtsTask({
required this.index,
required this.text,
required this.status,
});
}
/// TTS任务状态枚举
enum TtsTaskStatus {
ready, // 就绪
completed, // 已完成
failed, // 失败
}