610 lines
19 KiB
Dart
610 lines
19 KiB
Dart
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, // 失败
|
||
}
|