295 lines
9.3 KiB
C++
295 lines
9.3 KiB
C++
|
|
#include "TTSManager.h"
|
|||
|
|
#include <QDebug>
|
|||
|
|
#include <QDir>
|
|||
|
|
#include <algorithm>
|
|||
|
|
|
|||
|
|
TTSManager::TTSManager(QObject *parent)
|
|||
|
|
: QObject(parent),
|
|||
|
|
m_currentState(PlayState::Stopped)
|
|||
|
|
{
|
|||
|
|
// 初始化进程(用于执行 edge-tts + ffplay 命令)
|
|||
|
|
m_voiceProcess = new QProcess(this);
|
|||
|
|
connect(m_voiceProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
|||
|
|
this, &TTSManager::onPlayFinished);
|
|||
|
|
|
|||
|
|
// 初始化任务调度定时器(单触发模式,避免任务重叠)
|
|||
|
|
m_taskTimer = new QTimer(this);
|
|||
|
|
m_taskTimer->setSingleShot(true);
|
|||
|
|
m_taskTimer->setInterval(200); // 任务切换间隔(避免进程占用)
|
|||
|
|
connect(m_taskTimer, &QTimer::timeout, this, &TTSManager::processNextTask);
|
|||
|
|
|
|||
|
|
// 初始化临时音频路径(用户目录下,避免权限问题)
|
|||
|
|
m_tempAudioPath = QDir::homePath() + "/tts_temp_audio.wav";
|
|||
|
|
|
|||
|
|
// 检查环境是否就绪
|
|||
|
|
if (!checkEnvironment()) {
|
|||
|
|
qCritical() << "[TTS] 环境检查失败!请按提示安装依赖";
|
|||
|
|
m_currentState = PlayState::Failed;
|
|||
|
|
emit stateChanged(m_currentState);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TTSManager::~TTSManager()
|
|||
|
|
{
|
|||
|
|
// 析构时停止所有任务并清理
|
|||
|
|
stopAll();
|
|||
|
|
delete m_voiceProcess;
|
|||
|
|
delete m_taskTimer;
|
|||
|
|
// 删除残留的临时音频文件
|
|||
|
|
if (QFile::exists(m_tempAudioPath)) {
|
|||
|
|
QFile::remove(m_tempAudioPath);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 对外播放接口
|
|||
|
|
PlayState TTSManager::speak(const QString &text, int repeatCount, int priority)
|
|||
|
|
{
|
|||
|
|
// 环境未就绪,直接返回失败
|
|||
|
|
if (m_currentState == PlayState::Failed || !checkEnvironment()) {
|
|||
|
|
qWarning() << "[TTS] 播放失败:环境未就绪";
|
|||
|
|
return PlayState::Failed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 文本为空,返回失败
|
|||
|
|
QString trimmedText = text.trimmed();
|
|||
|
|
if (trimmedText.isEmpty()) {
|
|||
|
|
qWarning() << "[TTS] 播放失败:文本为空";
|
|||
|
|
return PlayState::Failed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理参数合法性(重复次数≥1,优先级0-3)
|
|||
|
|
int actualRepeat = qMax(1, repeatCount);
|
|||
|
|
int actualPriority = qBound(0, priority, 3);
|
|||
|
|
SpeechTask newTask = {trimmedText, actualRepeat, actualPriority, 0};
|
|||
|
|
|
|||
|
|
// 检查是否需要打断当前低优先级任务
|
|||
|
|
if (handleHighPriorityTask(newTask)) {
|
|||
|
|
return m_currentState;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 任务入队,若当前无播放则触发调度
|
|||
|
|
enqueueTask(newTask);
|
|||
|
|
if (m_currentState == PlayState::Stopped || m_currentState == PlayState::Completed) {
|
|||
|
|
m_taskTimer->start();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return m_currentState;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 停止所有播放任务
|
|||
|
|
void TTSManager::stopAll()
|
|||
|
|
{
|
|||
|
|
// 停止当前进程
|
|||
|
|
if (m_voiceProcess->state() == QProcess::Running) {
|
|||
|
|
m_voiceProcess->kill();
|
|||
|
|
m_voiceProcess->waitForFinished(500); // 等待进程退出
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清空任务队列和状态
|
|||
|
|
m_taskQueue.clear();
|
|||
|
|
m_currentTask = SpeechTask();
|
|||
|
|
m_currentState = PlayState::Stopped;
|
|||
|
|
m_taskTimer->stop();
|
|||
|
|
|
|||
|
|
// 删除临时音频文件
|
|||
|
|
if (QFile::exists(m_tempAudioPath)) {
|
|||
|
|
QFile::remove(m_tempAudioPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送状态变化信号
|
|||
|
|
emit stateChanged(m_currentState);
|
|||
|
|
qDebug() << "[TTS] 所有任务已停止";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 补充修改 onPlayFinished 函数,确保抢占后状态正确
|
|||
|
|
void TTSManager::onPlayFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
|||
|
|
{
|
|||
|
|
// 检查是否是被高优先级任务中断的情况
|
|||
|
|
if (exitCode == 1 && m_currentState == PlayState::Playing) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 原有逻辑:清理临时文件 + 处理重复播放 + 调度下一个任务
|
|||
|
|
if (QFile::exists(m_tempAudioPath)) {
|
|||
|
|
QFile::remove(m_tempAudioPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (m_currentTask.currentRepeat < m_currentTask.repeatCount - 1) {
|
|||
|
|
m_currentTask.currentRepeat++;
|
|||
|
|
|
|||
|
|
|
|||
|
|
if (executeVoiceTask(m_currentTask.text)) {
|
|||
|
|
m_currentState = PlayState::Playing;
|
|||
|
|
emit stateChanged(m_currentState);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
m_currentState = PlayState::Completed;
|
|||
|
|
emit stateChanged(m_currentState);
|
|||
|
|
m_currentTask = SpeechTask();
|
|||
|
|
m_taskTimer->start();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理下一个任务(从队列中取优先级最高的)
|
|||
|
|
void TTSManager::processNextTask()
|
|||
|
|
{
|
|||
|
|
// 队列空则切换到停止状态
|
|||
|
|
if (m_taskQueue.isEmpty()) {
|
|||
|
|
m_currentState = PlayState::Stopped;
|
|||
|
|
emit stateChanged(m_currentState);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 取出队列头部任务(已按优先级排序,头部优先级最高)
|
|||
|
|
m_currentTask = m_taskQueue.takeFirst();
|
|||
|
|
m_currentTask.currentRepeat = 0; // 重置重复计数
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 执行任务,更新状态
|
|||
|
|
if (executeVoiceTask(m_currentTask.text)) {
|
|||
|
|
m_currentState = PlayState::Playing;
|
|||
|
|
} else {
|
|||
|
|
m_currentState = PlayState::Failed;
|
|||
|
|
// 任务失败,调度下一个
|
|||
|
|
m_taskTimer->start();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
emit stateChanged(m_currentState);
|
|||
|
|
emit currentTaskChanged(m_currentTask);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 核心:执行 edge-tts 生成音频 + ffplay 播放
|
|||
|
|
bool TTSManager::executeVoiceTask(const QString &text)
|
|||
|
|
{
|
|||
|
|
// 进程忙则返回失败
|
|||
|
|
if (m_voiceProcess->state() == QProcess::Running) {
|
|||
|
|
qWarning() << "[TTS] 进程忙,无法执行新任务";
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 步骤1:处理文本和路径的双引号转义(先复制原字符串,再修改临时变量)
|
|||
|
|
QString escapedText = text; // 先复制原始文本
|
|||
|
|
escapedText.replace("\"", "\\\""); // 仅修改临时变量
|
|||
|
|
|
|||
|
|
QString escapedAudioPath = m_tempAudioPath; // 先复制原始路径
|
|||
|
|
escapedAudioPath.replace("\"", "\\\""); // 仅修改临时变量
|
|||
|
|
|
|||
|
|
// 步骤2:拼接命令(使用处理后的临时变量)
|
|||
|
|
QString cmd = QString(
|
|||
|
|
"source ~/tts_venv/bin/activate && "
|
|||
|
|
"edge-tts "
|
|||
|
|
"--voice zh-CN-XiaoxiaoNeural "
|
|||
|
|
"--text \"%1\" "
|
|||
|
|
"--write-media %2 && "
|
|||
|
|
"ffplay -autoexit -nodisp %2"
|
|||
|
|
).arg(escapedText, escapedAudioPath);
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 执行命令(通过 bash 解释器,支持 source 命令)
|
|||
|
|
|
|||
|
|
m_voiceProcess->start("bash", QStringList() << "-c" << cmd);
|
|||
|
|
|
|||
|
|
// 等待进程启动(超时1秒)
|
|||
|
|
if (!m_voiceProcess->waitForStarted(1000)) {
|
|||
|
|
qCritical() << "[TTS] 进程启动失败!请检查环境";
|
|||
|
|
m_voiceProcess->kill();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool TTSManager::checkEnvironment()
|
|||
|
|
{
|
|||
|
|
bool envReady = true;
|
|||
|
|
|
|||
|
|
// 1. 修复 edge-tts 检测(不用 --version,改用 --help 验证是否存在)
|
|||
|
|
QProcess ttsCheck;
|
|||
|
|
QString ttsCheckCmd = QString(
|
|||
|
|
"source /home/zmj/tts_venv/bin/activate && edge-tts --help" // 用 --help 替代 --version
|
|||
|
|
);
|
|||
|
|
ttsCheck.start("bash", QStringList() << "-c" << ttsCheckCmd);
|
|||
|
|
ttsCheck.waitForFinished(3000);
|
|||
|
|
|
|||
|
|
// 检查退出码(0 表示存在,非0表示不存在)
|
|||
|
|
if (ttsCheck.exitCode() != 0) {
|
|||
|
|
qWarning() << "[TTS] 未检测到 edge-tts!请执行:source /home/zmj/tts_venv/bin/activate && pip3 install edge-tts";
|
|||
|
|
envReady = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 修复 ffplay 检测(用 -version 单横线参数)
|
|||
|
|
QProcess ffplayCheck;
|
|||
|
|
ffplayCheck.start("/usr/bin/ffplay -version"); // 单横线 -version
|
|||
|
|
ffplayCheck.waitForFinished(2000);
|
|||
|
|
|
|||
|
|
if (ffplayCheck.exitCode() != 0) {
|
|||
|
|
qWarning() << "[TTS] 未检测到 ffplay!请执行:sudo apt-get install ffmpeg";
|
|||
|
|
envReady = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return envReady;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 入队任务(按优先级降序排序,优先级高的在前)
|
|||
|
|
void TTSManager::enqueueTask(const SpeechTask &task)
|
|||
|
|
{
|
|||
|
|
// 插入队列并按优先级排序(3>2>1>0)
|
|||
|
|
m_taskQueue.append(task);
|
|||
|
|
std::sort(m_taskQueue.begin(), m_taskQueue.end(),
|
|||
|
|
[](const SpeechTask &a, const SpeechTask &b) {
|
|||
|
|
return a.priority > b.priority;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 修改 handleHighPriorityTask 函数,强化抢占逻辑
|
|||
|
|
bool TTSManager::handleHighPriorityTask(const SpeechTask &newTask)
|
|||
|
|
{
|
|||
|
|
// 1. 当前无任务或新任务优先级不高于当前,无需抢占
|
|||
|
|
if (m_currentState != PlayState::Playing || newTask.priority <= m_currentTask.priority) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 高优先级任务需要抢占,记录当前任务状态以便后续恢复
|
|||
|
|
SpeechTask interruptedTask = m_currentTask;
|
|||
|
|
// 计算剩余重复次数(当前已播次数 + 剩余次数)
|
|||
|
|
interruptedTask.currentRepeat = m_currentTask.currentRepeat;
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 3. 强制停止当前播放进程(立即中断低优先级语音)
|
|||
|
|
if (m_voiceProcess->state() == QProcess::Running) {
|
|||
|
|
|
|||
|
|
m_voiceProcess->kill(); // 强制终止进程
|
|||
|
|
m_voiceProcess->waitForFinished(500); // 等待进程退出
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 被中断的任务重新入队(继续完成剩余播放次数)
|
|||
|
|
if (interruptedTask.currentRepeat < interruptedTask.repeatCount - 1) {
|
|||
|
|
// 更新剩余重复次数(减去已完成的1次)
|
|||
|
|
interruptedTask.repeatCount -= (interruptedTask.currentRepeat + 1);
|
|||
|
|
interruptedTask.currentRepeat = 0;
|
|||
|
|
enqueueTask(interruptedTask);
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. 立即执行高优先级任务
|
|||
|
|
m_currentTask = newTask;
|
|||
|
|
m_currentTask.currentRepeat = 0; // 重置重复计数
|
|||
|
|
if (executeVoiceTask(m_currentTask.text)) {
|
|||
|
|
m_currentState = PlayState::Playing;
|
|||
|
|
emit stateChanged(m_currentState);
|
|||
|
|
emit currentTaskChanged(m_currentTask);
|
|||
|
|
} else {
|
|||
|
|
m_currentState = PlayState::Failed;
|
|||
|
|
m_taskTimer->start(); // 若启动失败,调度下一个任务
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|