TTS和录制语音

XinqiQin 1 month ago
parent 169edb99ea
commit b8e435e598

@ -1,2 +1,5 @@
# Software_Architecture
requirement
sshpass
安装espeak-ng for TTS引擎

@ -0,0 +1,694 @@
#include "IntelligenceUI.h"
#include "ui_IntelligenceUI.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QFileDialog>
#include <QTimer>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QDebug>
#include <QFileInfo>
#include <QAudioRecorder>
#include <QAudioEncoderSettings>
#include <QMediaRecorder>
#include <QRandomGenerator>
IntelligenceUI::IntelligenceUI(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::IntelligenceUI)
, sshProcess(nullptr)
, fileTransferProcess(nullptr)
, m_localAudioPath("")
, audioRecorder(nullptr)
, audioInput(nullptr)
, volumeTimer(nullptr)
, ttsProcess(nullptr)
, ttsOutputPath("")
{
ui->setupUi(this);
// 初始化并加载SSH设置
updateSshSettings();
// 初始化录制功能
setupAudioRecorder();
// 初始化TTS功能
setupTTS();
// 连接信号槽
connect(ui->playSelectedAudio, &QPushButton::clicked, this, &IntelligenceUI::on_playSelectedAudio_clicked);
connect(ui->killWSAudio, &QPushButton::clicked, this, &IntelligenceUI::on_killWSAudio_clicked);
connect(ui->refreshAudioList, &QPushButton::clicked, this, &IntelligenceUI::on_refreshAudioList_clicked);
connect(ui->sshSettingsGroup->findChild<QPushButton*>("saveSshSettings"), &QPushButton::clicked, this, &IntelligenceUI::on_saveSshSettings_clicked);
connect(ui->browseButton, &QPushButton::clicked, this, &IntelligenceUI::on_browseButton_clicked);
connect(ui->uploadAndPlayButton, &QPushButton::clicked, this, &IntelligenceUI::on_uploadAndPlayButton_clicked);
// 连接录制相关信号槽
connect(ui->recordButton, &QPushButton::clicked, this, &IntelligenceUI::on_recordButton_clicked);
connect(ui->stopRecordButton, &QPushButton::clicked, this, &IntelligenceUI::on_stopRecordButton_clicked);
connect(ui->playRecordedButton, &QPushButton::clicked, this, &IntelligenceUI::on_playRecordedButton_clicked);
// 连接TTS相关信号槽
connect(ui->generateTtsButton, &QPushButton::clicked, this, &IntelligenceUI::on_generateTtsButton_clicked);
connect(ui->playTtsButton, &QPushButton::clicked, this, &IntelligenceUI::on_playTtsButton_clicked);
// 初始化状态
updateStatus("情报传达系统已启动,准备就绪");
ui->progressBar->setValue(0);
}
IntelligenceUI::~IntelligenceUI()
{
if (sshProcess && sshProcess->state() != QProcess::NotRunning) {
sshProcess->kill();
sshProcess->waitForFinished(3000);
}
delete ui;
}
void IntelligenceUI::executeSSHCommand(const QString &command, const QString &description)
{
if (sshProcess && sshProcess->state() != QProcess::NotRunning) {
updateStatus("上一个命令仍在执行中,请稍候...", true);
return;
}
if (!sshProcess) {
sshProcess = new QProcess(this);
connect(sshProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &IntelligenceUI::onSshProcessFinished);
connect(sshProcess, &QProcess::errorOccurred,
this, &IntelligenceUI::onSshProcessError);
}
currentCommand = description;
updateStatus(QString("正在执行: %1").arg(description));
ui->progressBar->setValue(25);
// 1. 从UI获取最新的目标板卡设置
updateSshSettings();
// 2. 定义跳板机和目标板卡的连接信息
QString jumpHost = "pi@192.168.12.1";
QString jumpPassword = "123";
QString targetHost = QString("%1@%2").arg(m_sshUser).arg(m_sshHost);
QString targetPassword = m_sshPassword;
// 3. 简化的SSH命令 - 直接用单条命令链接
QString escapedCommand = command;
escapedCommand.replace("'", "'\"'\"'"); // 转义单引号
QString fullCommand = QString(
"sshpass -p '%1' ssh -T -n -o StrictHostKeyChecking=no -o ConnectTimeout=10 %2 "
"\"sshpass -p '%3' ssh -T -n -o StrictHostKeyChecking=no -o ConnectTimeout=10 %4 '%5'\""
).arg(jumpPassword)
.arg(jumpHost)
.arg(targetPassword)
.arg(targetHost)
.arg(escapedCommand);
qDebug() << "执行SSH命令:" << fullCommand;
sshProcess->start("bash", QStringList() << "-c" << fullCommand);
}
void IntelligenceUI::on_playSelectedAudio_clicked()
{
QString selectedAudio = ui->audioComboBox->currentText();
playAudioFile(selectedAudio);
}
void IntelligenceUI::on_killWSAudio_clicked()
{
killWSAudioProcess();
}
void IntelligenceUI::on_refreshAudioList_clicked()
{
refreshAudioFileList();
}
void IntelligenceUI::on_saveSshSettings_clicked()
{
updateSshSettings();
updateStatus("SSH连接设置已更新并保存。");
}
void IntelligenceUI::updateSshSettings()
{
m_sshHost = ui->lineEditIp->text();
m_sshUser = ui->lineEditUsername->text();
m_sshPassword = ui->lineEditPassword->text();
ui->deviceLabel->setText(QString("当前目标: %1 (%2)").arg(m_sshHost).arg(m_sshUser));
}
void IntelligenceUI::killWSAudioProcess()
{
QString command_template = "pids=$(ps -aux | grep wsaudio | grep -v grep | awk '{print $2}'); "
"if [ ! -z \"$pids\" ]; then "
"echo \"找到wsaudio进程: $pids\"; "
"echo '%1' | sudo -S kill -9 $pids; "
"echo \"已终止wsaudio进程\"; "
"else "
"echo \"未找到wsaudio进程\"; "
"fi";
QString command = command_template.arg(m_sshPassword);
executeSSHCommand(command, "解除wsaudio音频占用");
}
void IntelligenceUI::playAudioFile(const QString &audioFile)
{
// 根据您的手动操作日志,文件路径为 audio_file/
QString remote_audio_path = "audio_file/" + audioFile;
// 这是最终在目标板卡上执行的脚本,使用换行符使其更清晰
QString command_script_template = QString(
"pids=$(ps -aux | grep wsaudio | grep -v grep | awk '{print $2}')\n"
"if [ ! -z \"$pids\" ]; then\n"
" echo \"检测到wsaudio进程正在终止...\"\n"
" echo '%1' | sudo -S kill -9 $pids\n"
" sleep 1\n"
"fi\n"
"echo \"开始播放音频: %2\"\n"
"aplay -D plughw:2,0 %2"
);
QString command_script = command_script_template.arg(m_sshPassword).arg(remote_audio_path);
executeSSHCommand(command_script, QString("播放音频文件: %1").arg(audioFile));
}
void IntelligenceUI::refreshAudioFileList()
{
// 根据手动操作日志,文件位于 audio_file/ 目录
QString command = "ls audio_file/*.wav 2>/dev/null || echo '未找到wav文件'";
executeSSHCommand(command, "刷新音频文件列表");
}
void IntelligenceUI::onSshProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
ui->progressBar->setValue(100);
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
updateStatus(QString("%1 - 执行成功").arg(currentCommand));
} else {
updateStatus(QString("%1 - 执行失败 (退出码: %2)").arg(currentCommand).arg(exitCode), true);
}
// 读取命令输出
if (sshProcess) {
QByteArray output = sshProcess->readAllStandardOutput();
QByteArray error = sshProcess->readAllStandardError();
if (!output.isEmpty()) {
updateStatus(QString("输出: %1").arg(QString::fromUtf8(output).trimmed()));
}
if (!error.isEmpty()) {
updateStatus(QString("错误: %1").arg(QString::fromUtf8(error).trimmed()), true);
}
}
// 强制清理进程,确保下次能正常执行
if (sshProcess) {
sshProcess->kill(); // 强制终止
sshProcess->waitForFinished(1000); // 等待最多1秒
sshProcess->deleteLater();
sshProcess = nullptr;
}
// 重置进度条
QTimer::singleShot(2000, [this]() {
ui->progressBar->setValue(0);
});
}
void IntelligenceUI::onSshProcessError(QProcess::ProcessError error)
{
ui->progressBar->setValue(0);
QString errorString;
switch (error) {
case QProcess::FailedToStart:
errorString = "命令启动失败";
break;
case QProcess::Crashed:
errorString = "命令执行崩溃";
break;
case QProcess::Timedout:
errorString = "命令执行超时";
break;
default:
errorString = "未知错误";
break;
}
updateStatus(QString("%1 - %2").arg(currentCommand).arg(errorString), true);
}
void IntelligenceUI::updateStatus(const QString &message, bool isError)
{
QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss");
QString logMessage = QString("[%1] %2").arg(timestamp).arg(message);
if (isError) {
ui->logTextEdit->setTextColor(QColor(255, 100, 100));
} else {
ui->logTextEdit->setTextColor(QColor(100, 255, 100));
}
ui->logTextEdit->append(logMessage);
ui->logTextEdit->setTextColor(QColor(220, 230, 240)); // 重置颜色
// 自动滚动到底部
ui->logTextEdit->moveCursor(QTextCursor::End);
}
void IntelligenceUI::on_browseButton_clicked()
{
QString filePath = QFileDialog::getOpenFileName(this, "选择音频文件", QDir::homePath(), "音频文件 (*.wav)");
if (!filePath.isEmpty()) {
m_localAudioPath = filePath;
ui->filePathLineEdit->setText(filePath);
updateStatus(QString("已选择文件: %1").arg(QFileInfo(filePath).fileName()));
}
}
void IntelligenceUI::on_uploadAndPlayButton_clicked()
{
if (m_localAudioPath.isEmpty()) {
updateStatus("错误: 请先选择一个要上传的音频文件。", true);
return;
}
if (fileTransferProcess && fileTransferProcess->state() != QProcess::NotRunning) {
updateStatus("上一个文件传输仍在进行中,请稍候...", true);
return;
}
if (!fileTransferProcess) {
fileTransferProcess = new QProcess(this);
connect(fileTransferProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &IntelligenceUI::onFileUploadFinished);
}
QFileInfo fileInfo(m_localAudioPath);
QString fileName = fileInfo.fileName();
QString remotePath = "audio_file/" + fileName;
currentCommand = QString("上传并播放: %1").arg(fileName);
updateStatus(QString("正在上传文件: %1...").arg(fileName));
ui->progressBar->setValue(10);
// --- 使用cat和管道进行文件传输 ---
updateSshSettings();
QString jumpHost = "pi@192.168.12.1";
QString jumpPassword = "123";
QString targetHost = QString("%1@%2").arg(m_sshUser).arg(m_sshHost);
QString targetPassword = m_sshPassword;
const QString commandTemplate =
"cat %1 | sshpass -p '%2' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 %3 "
"\"sshpass -p '%4' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 %5 'cat > %6'\"";
QString fullCommand = QString(commandTemplate)
.arg(m_localAudioPath) // %1: 本地文件路径
.arg(jumpPassword) // %2: 跳板机密码
.arg(jumpHost) // %3: 跳板机地址
.arg(targetPassword) // %4: 目标板卡密码
.arg(targetHost) // %5: 目标板卡地址
.arg(remotePath); // %6: 远程文件路径
qDebug() << "执行文件上传命令:" << fullCommand;
fileTransferProcess->start("bash", QStringList() << "-c" << fullCommand);
}
void IntelligenceUI::onFileUploadFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
QFileInfo fileInfo(m_localAudioPath);
QString fileName = fileInfo.fileName();
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
updateStatus(QString("文件 '%1' 上传成功。").arg(fileName));
ui->progressBar->setValue(50);
// 上传成功后,立即播放该文件
playAudioFile(fileName);
} else {
updateStatus(QString("文件 '%1' 上传失败 (退出码: %2)").arg(fileName).arg(exitCode), true);
ui->progressBar->setValue(0);
}
}
void IntelligenceUI::setupAudioRecorder()
{
// 创建音频录制器
audioRecorder = new QAudioRecorder(this);
// 设置音频格式 - 匹配机器狗需要的格式
QAudioEncoderSettings audioSettings;
audioSettings.setCodec("audio/pcm");
audioSettings.setSampleRate(22050); // 22kHz采样率匹配warning.wav
audioSettings.setBitRate(176400); // 8位单声道的比特率
audioSettings.setChannelCount(1); // 单声道
audioSettings.setQuality(QMultimedia::NormalQuality);
audioRecorder->setAudioSettings(audioSettings);
audioRecorder->setContainerFormat("wav");
// 连接录制相关信号
connect(audioRecorder, &QAudioRecorder::durationChanged, this, [this](qint64 duration) {
updateRecordingStatus(QString("录制中... %1秒").arg(duration / 1000));
});
connect(audioRecorder, &QAudioRecorder::statusChanged, this, [this](QMediaRecorder::Status status) {
if (status == QMediaRecorder::UnavailableStatus || status == QMediaRecorder::UnloadedStatus) {
onRecordingFinished();
}
});
connect(audioRecorder, QOverload<QMediaRecorder::Error>::of(&QAudioRecorder::error),
this, &IntelligenceUI::onRecordingError);
// 创建音量监测定时器
volumeTimer = new QTimer(this);
connect(volumeTimer, &QTimer::timeout, this, &IntelligenceUI::updateAudioLevel);
updateRecordingStatus("录制系统就绪");
}
void IntelligenceUI::on_recordButton_clicked()
{
if (!audioRecorder) {
updateStatus("错误: 录制器未初始化", true);
return;
}
// 创建录制文件路径
QString recordingsDir = QDir::currentPath() + "/recordings";
QDir().mkpath(recordingsDir);
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss");
recordedFilePath = recordingsDir + QString("/recorded_%1.wav").arg(timestamp);
updateStatus(QString("准备录制到: %1").arg(recordedFilePath));
audioRecorder->setOutputLocation(QUrl::fromLocalFile(recordedFilePath));
// 开始录制
audioRecorder->record();
enableRecordingControls(true);
updateRecordingStatus("正在录制...");
updateStatus("开始录制语音");
// 启动音量监测
volumeTimer->start(100); // 每100ms更新一次音量显示
}
void IntelligenceUI::on_stopRecordButton_clicked()
{
updateStatus("尝试停止录制...");
if (!audioRecorder) {
updateStatus("错误: 录制器为空", true);
return;
}
QMediaRecorder::State currentState = audioRecorder->state();
updateStatus(QString("当前录制状态: %1").arg(currentState));
// 无论当前状态如何,都尝试停止
audioRecorder->stop();
volumeTimer->stop();
ui->volumeMeter->setValue(0);
enableRecordingControls(false);
updateRecordingStatus("正在停止录制...");
// 延迟检查文件是否生成
QTimer::singleShot(1000, this, [this]() {
if (QFile::exists(recordedFilePath)) {
QFileInfo fileInfo(recordedFilePath);
updateRecordingStatus(QString("录制完成 - %1 (%2 KB)").arg(fileInfo.fileName()).arg(fileInfo.size() / 1024));
updateStatus(QString("录制完成,文件保存至: %1").arg(recordedFilePath));
// 启用试听按钮
ui->playRecordedButton->setEnabled(true);
// 自动设置为要上传的文件
m_localAudioPath = recordedFilePath;
ui->filePathLineEdit->setText(recordedFilePath);
} else {
updateRecordingStatus("录制可能失败,未找到文件");
updateStatus(QString("录制文件未找到: %1").arg(recordedFilePath), true);
}
});
}
void IntelligenceUI::on_playRecordedButton_clicked()
{
if (recordedFilePath.isEmpty() || !QFile::exists(recordedFilePath)) {
updateStatus("错误: 没有找到录制的音频文件", true);
return;
}
// 使用系统默认播放器试听录制的音频
QProcess *playProcess = new QProcess(this);
connect(playProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
playProcess, &QProcess::deleteLater);
// 尝试使用不同的音频播放命令
#ifdef Q_OS_LINUX
playProcess->start("aplay", QStringList() << recordedFilePath);
#elif defined(Q_OS_WIN)
playProcess->start("powershell", QStringList() << "-c" << QString("(New-Object Media.SoundPlayer '%1').PlaySync()").arg(recordedFilePath));
#elif defined(Q_OS_MAC)
playProcess->start("afplay", QStringList() << recordedFilePath);
#endif
updateStatus("正在本地试听录制的音频...");
}
void IntelligenceUI::onRecordingFinished()
{
enableRecordingControls(false);
volumeTimer->stop();
ui->volumeMeter->setValue(0);
if (QFile::exists(recordedFilePath)) {
QFileInfo fileInfo(recordedFilePath);
updateRecordingStatus(QString("录制完成 - %1 (%2 KB)").arg(fileInfo.fileName()).arg(fileInfo.size() / 1024));
updateStatus(QString("录制完成: %1").arg(fileInfo.fileName()));
} else {
updateRecordingStatus("录制失败");
updateStatus("录制失败: 文件未生成", true);
}
}
void IntelligenceUI::onRecordingError(QMediaRecorder::Error error)
{
QString errorString;
switch (error) {
case QMediaRecorder::NoError:
return;
case QMediaRecorder::ResourceError:
errorString = "资源错误";
break;
case QMediaRecorder::FormatError:
errorString = "格式错误";
break;
case QMediaRecorder::OutOfSpaceError:
errorString = "磁盘空间不足";
break;
default:
errorString = "未知错误";
break;
}
updateRecordingStatus("录制错误: " + errorString);
updateStatus("录制错误: " + errorString, true);
enableRecordingControls(false);
volumeTimer->stop();
ui->volumeMeter->setValue(0);
}
void IntelligenceUI::updateAudioLevel()
{
if (!audioRecorder || audioRecorder->state() != QMediaRecorder::RecordingState) {
return;
}
// 简化音量显示 - 模拟录制时的音量指示
static int volumeCounter = 0;
volumeCounter = (volumeCounter + 1) % 100;
// 模拟音量波动(在实际项目中可以通过其他方式获取真实音量)
int volumeLevel = 50 + (QRandomGenerator::global()->bounded(30)); // 50-80之间的随机值
ui->volumeMeter->setValue(volumeLevel);
}
void IntelligenceUI::updateRecordingStatus(const QString &status)
{
ui->recordStatusLabel->setText("录制状态: " + status);
}
void IntelligenceUI::enableRecordingControls(bool recording)
{
ui->recordButton->setEnabled(!recording);
ui->stopRecordButton->setEnabled(recording);
if (recording) {
ui->recordButton->setText("🎤 录制中...");
ui->recordButton->setStyleSheet(
"QPushButton { background-color: rgb(165, 85, 45); }"
);
} else {
ui->recordButton->setText("🎤 开始录制");
ui->recordButton->setStyleSheet(
"QPushButton { background-color: rgb(45, 125, 65); }"
"QPushButton:hover { background-color: rgb(65, 145, 85); }"
"QPushButton:pressed { background-color: rgb(55, 135, 75); }"
);
}
}
// ========== TTS相关功能实现 ==========
void IntelligenceUI::setupTTS()
{
ttsProcess = nullptr;
updateTtsStatus("TTS系统就绪");
}
void IntelligenceUI::on_generateTtsButton_clicked()
{
QString text = ui->ttsTextEdit->toPlainText().trimmed();
if (text.isEmpty()) {
updateStatus("错误: 请输入要转换的文字内容", true);
return;
}
if (ttsProcess && ttsProcess->state() != QProcess::NotRunning) {
updateStatus("TTS转换正在进行中请稍候...", true);
return;
}
// 创建TTS输出目录
QString ttsDir = QDir::currentPath() + "/tts_output";
QDir().mkpath(ttsDir);
// 生成输出文件名
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss");
ttsOutputPath = ttsDir + QString("/tts_%1.wav").arg(timestamp);
// 获取选择的语音类型
QString voiceType = ui->voiceComboBox->currentText();
QString espeakVoice;
if (voiceType == "标准女声") {
espeakVoice = "cmn"; // 中文普通话
} else if (voiceType == "标准男声") {
espeakVoice = "cmn"; // 中文普通话
} else if (voiceType == "儿童声") {
espeakVoice = "cmn"; // 中文普通话
} else {
espeakVoice = "cmn"; // 默认中文普通话
}
updateTtsStatus("正在生成语音...");
updateStatus(QString("正在将文字转换为语音: %1").arg(text.left(50) + (text.length() > 50 ? "..." : "")));
// 创建TTS进程
if (!ttsProcess) {
ttsProcess = new QProcess(this);
connect(ttsProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &IntelligenceUI::onTtsGenerationFinished);
}
// 构建espeak命令
// -v: 语音类型, -s: 语速, -a: 音量, -w: 输出到WAV文件
QStringList arguments;
arguments << "-v" << espeakVoice
<< "-s" << "150" // 语速 150 wpm
<< "-a" << "100" // 音量 100
<< "-w" << ttsOutputPath // 输出文件
<< text; // 要转换的文字
qDebug() << "TTS命令:" << "espeak-ng" << arguments.join(" ");
ttsProcess->start("espeak-ng", arguments);
}
void IntelligenceUI::on_playTtsButton_clicked()
{
if (ttsOutputPath.isEmpty() || !QFile::exists(ttsOutputPath)) {
updateStatus("错误: 没有找到TTS生成的音频文件", true);
return;
}
// 使用系统默认播放器试听TTS音频
QProcess *playProcess = new QProcess(this);
connect(playProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
playProcess, &QProcess::deleteLater);
// 尝试使用不同的音频播放命令
#ifdef Q_OS_LINUX
playProcess->start("aplay", QStringList() << ttsOutputPath);
#elif defined(Q_OS_WIN)
playProcess->start("powershell", QStringList() << "-c" << QString("(New-Object Media.SoundPlayer '%1').PlaySync()").arg(ttsOutputPath));
#elif defined(Q_OS_MAC)
playProcess->start("afplay", QStringList() << ttsOutputPath);
#endif
updateStatus("正在本地试听TTS生成的音频...");
}
void IntelligenceUI::onTtsGenerationFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
if (QFile::exists(ttsOutputPath)) {
QFileInfo fileInfo(ttsOutputPath);
updateTtsStatus(QString("TTS完成 - %1 (%2 KB)").arg(fileInfo.fileName()).arg(fileInfo.size() / 1024));
updateStatus(QString("TTS生成成功: %1").arg(fileInfo.fileName()));
// 启用试听按钮
ui->playTtsButton->setEnabled(true);
// 自动设置为要上传的文件
m_localAudioPath = ttsOutputPath;
ui->filePathLineEdit->setText(ttsOutputPath);
updateStatus("TTS音频已自动设置为上传文件可直接点击'上传并播放'");
} else {
updateTtsStatus("TTS失败: 文件未生成");
updateStatus("TTS生成失败: 文件未生成", true);
}
} else {
updateTtsStatus("TTS生成失败");
updateStatus(QString("TTS生成失败 (退出码: %1)").arg(exitCode), true);
// 读取错误信息
if (ttsProcess) {
QByteArray error = ttsProcess->readAllStandardError();
if (!error.isEmpty()) {
updateStatus(QString("TTS错误: %1").arg(QString::fromUtf8(error).trimmed()), true);
}
}
}
// 清理进程
if (ttsProcess) {
ttsProcess->deleteLater();
ttsProcess = nullptr;
}
}
void IntelligenceUI::updateTtsStatus(const QString &status)
{
ui->ttsStatusLabel->setText("TTS状态: " + status);
}

@ -0,0 +1,121 @@
#ifndef INTELLIGENCEUI_H
#define INTELLIGENCEUI_H
#include <QMainWindow>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QProcess>
#include <QMessageBox>
#include <QDebug>
#include <QComboBox>
#include <QTextEdit>
#include <QProgressBar>
#include <QFileDialog>
#include <QTimer>
#include <QMediaRecorder>
#include <QAudioRecorder>
#include <QAudioInput>
#include <QAudioFormat>
#include <QUrl>
#include <QDir>
QT_BEGIN_NAMESPACE
namespace Ui {
class IntelligenceUI;
}
QT_END_NAMESPACE
class IntelligenceUI : public QMainWindow
{
Q_OBJECT
public:
IntelligenceUI(QWidget *parent = nullptr);
~IntelligenceUI();
private slots:
// 播放音频按钮
void on_playSelectedAudio_clicked();
// 杀死wsaudio进程按钮
void on_killWSAudio_clicked();
// 刷新音频文件列表
void on_refreshAudioList_clicked();
// 保存SSH连接设置
void on_saveSshSettings_clicked();
// 上传并播放
void on_browseButton_clicked();
void on_uploadAndPlayButton_clicked();
void onFileUploadFinished(int exitCode, QProcess::ExitStatus exitStatus);
// SSH进程处理
void onSshProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
void onSshProcessError(QProcess::ProcessError error);
// 新增录制功能相关槽函数
void on_recordButton_clicked();
void on_stopRecordButton_clicked();
void on_playRecordedButton_clicked();
// 录制相关处理
void onRecordingFinished();
void onRecordingError(QMediaRecorder::Error error);
void updateAudioLevel();
// 新增TTS功能相关槽函数
void on_generateTtsButton_clicked();
void on_playTtsButton_clicked();
void onTtsGenerationFinished(int exitCode, QProcess::ExitStatus exitStatus);
private:
Ui::IntelligenceUI *ui;
QProcess *sshProcess;
QProcess *fileTransferProcess;
QString currentCommand;
// SSH连接信息
QString m_sshHost;
QString m_sshUser;
QString m_sshPassword;
// 本地文件路径
QString m_localAudioPath;
// 录制相关
QAudioRecorder *audioRecorder;
QAudioInput *audioInput;
QTimer *volumeTimer;
QString recordedFilePath;
// TTS相关
QProcess *ttsProcess;
QString ttsOutputPath;
// 核心方法
void executeSSHCommand(const QString &command, const QString &description);
//qxq:
void killWSAudioProcess();
void playAudioFile(const QString &audioFile);
void refreshAudioFileList();
void updateSshSettings();
// UI设置
void setupUI();
void updateStatus(const QString &message, bool isError = false);
// 录制相关私有方法
void setupAudioRecorder();
void updateRecordingStatus(const QString &status);
void enableRecordingControls(bool recording);
// TTS相关私有方法
void setupTTS();
void updateTtsStatus(const QString &status);
};
#endif // INTELLIGENCEUI_H

@ -0,0 +1,634 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>IntelligenceUI</class>
<widget class="QMainWindow" name="IntelligenceUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>800</height>
</rect>
</property>
<property name="windowTitle">
<string>情报传达系统 - UnitreeGo1</string>
</property>
<property name="styleSheet">
<string notr="true">QMainWindow {
background-color: rgb(24, 33, 45);
}
QPushButton {
background-color: rgb(30, 44, 62);
color: rgb(220, 230, 240);
border: 2px solid rgba(82, 194, 242, 0.5);
border-radius: 8px;
padding: 12px 20px;
font-size: 14px;
font-weight: bold;
min-height: 35px;
}
QPushButton:hover {
background-color: rgb(50, 70, 95);
border: 2px solid rgba(82, 194, 242, 0.8);
}
QPushButton:pressed {
background-color: rgb(40, 60, 85);
border: 2px solid rgba(82, 194, 242, 1.0);
}
QLabel {
color: rgb(220, 230, 240);
font-size: 14px;
}
QComboBox {
background-color: rgb(30, 44, 62);
color: rgb(220, 230, 240);
border: 2px solid rgba(82, 194, 242, 0.5);
border-radius: 5px;
padding: 8px;
font-size: 14px;
}
QTextEdit {
background-color: rgb(15, 22, 32);
color: rgb(220, 230, 240);
border: 2px solid rgba(82, 194, 242, 0.3);
border-radius: 5px;
font-family: "Courier New", monospace;
font-size: 12px;
}
QProgressBar {
border: 2px solid rgba(82, 194, 242, 0.5);
border-radius: 5px;
text-align: center;
background-color: rgb(30, 44, 62);
color: rgb(220, 230, 240);
}
QProgressBar::chunk {
background-color: rgba(82, 194, 242, 0.8);
border-radius: 3px;
}</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>20</number>
</property>
<property name="leftMargin">
<number>30</number>
</property>
<property name="topMargin">
<number>20</number>
</property>
<property name="rightMargin">
<number>30</number>
</property>
<property name="bottomMargin">
<number>20</number>
</property>
<item>
<widget class="QLabel" name="titleLabel">
<property name="text">
<string>🔊 情报传达系统</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="styleSheet">
<string notr="true">QLabel {
color: rgb(82, 194, 242);
font-size: 32px;
font-weight: bold;
padding: 20px;
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 rgba(82, 194, 242, 0.1),
stop:1 rgba(45, 120, 180, 0.1));
border: 2px solid rgba(82, 194, 242, 0.3);
border-radius: 10px;
}</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="sshSettingsGroup">
<property name="title">
<string>SSH 连接设置</string>
</property>
<property name="styleSheet">
<string notr="true">QGroupBox {
font-size: 16px;
font-weight: bold;
color: rgb(220, 230, 240);
border: 1px solid rgba(82, 194, 242, 0.4);
border-radius: 8px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 10px;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelIp">
<property name="text">
<string>目标 IP:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEditIp">
<property name="text">
<string>192.168.123.13</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelUsername">
<property name="text">
<string>用户名:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEditUsername">
<property name="text">
<string>unitree</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelPassword">
<property name="text">
<string>密码:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="lineEditPassword">
<property name="text">
<string>123</string>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="saveSshSettings">
<property name="text">
<string>保存并应用设置</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="deviceLabel">
<property name="text">
<string>当前目标: 192.168.123.13 (UnitreeGo1)</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(160, 170, 180); font-size: 16px;</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="audioSelectionLayout">
<item>
<widget class="QLabel" name="audioLabel">
<property name="text">
<string>选择音频文件:</string>
</property>
<property name="styleSheet">
<string notr="true">font-size: 16px; font-weight: bold;</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="audioComboBox">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<item>
<property name="text">
<string>warning.wav</string>
</property>
</item>
<item>
<property name="text">
<string>alert.wav</string>
</property>
</item>
<item>
<property name="text">
<string>emergency.wav</string>
</property>
</item>
<item>
<property name="text">
<string>notification.wav</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="refreshAudioList">
<property name="text">
<string>刷新列表</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="controlButtonsLayout">
<item>
<widget class="QPushButton" name="playSelectedAudio">
<property name="text">
<string>🔊 播放选定音频</string>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
background-color: rgb(45, 125, 65);
font-size: 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgb(65, 145, 85);
}
QPushButton:pressed {
background-color: rgb(55, 135, 75);
}</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="killWSAudio">
<property name="text">
<string>🔧 解除音频占用</string>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
background-color: rgb(165, 85, 45);
font-size: 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgb(185, 105, 65);
}
QPushButton:pressed {
background-color: rgb(175, 95, 55);
}</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="uploadAudioGroup">
<property name="title">
<string>自定义音频制作</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="recordLabel">
<property name="text">
<string>🎤 录制语音:</string>
</property>
<property name="styleSheet">
<string notr="true">font-size: 14px; font-weight: bold; color: rgb(82, 194, 242);</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="recordLayout">
<item>
<widget class="QPushButton" name="recordButton">
<property name="text">
<string>🎤 开始录制</string>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
background-color: rgb(45, 125, 65);
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgb(65, 145, 85);
}
QPushButton:pressed {
background-color: rgb(55, 135, 75);
}</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopRecordButton">
<property name="text">
<string>⏹ 停止录制</string>
</property>
<property name="enabled">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
background-color: rgb(165, 85, 45);
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgb(185, 105, 65);
}
QPushButton:pressed {
background-color: rgb(175, 95, 55);
}</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="playRecordedButton">
<property name="text">
<string>▶ 试听录音</string>
</property>
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="recordStatusLabel">
<property name="text">
<string>录制状态: 就绪</string>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(160, 170, 180); font-size: 12px;</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="volumeMeter">
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">QProgressBar {
border: 1px solid rgba(82, 194, 242, 0.5);
border-radius: 3px;
background-color: rgb(30, 44, 62);
height: 10px;
}
QProgressBar::chunk {
background-color: rgba(45, 200, 45, 0.8);
border-radius: 2px;
}</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="orLabel">
<property name="text">
<string>或者</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(160, 170, 180); font-size: 12px; margin: 10px;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="ttsLabel">
<property name="text">
<string>🗣 文字转语音:</string>
</property>
<property name="styleSheet">
<string notr="true">font-size: 14px; font-weight: bold; color: rgb(82, 194, 242);</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="ttsTextEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>80</height>
</size>
</property>
<property name="placeholderText">
<string>输入要转换为语音的文字内容...</string>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(30, 44, 62); border: 1px solid rgba(82, 194, 242, 0.3); border-radius: 3px; padding: 5px;</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="ttsLayout">
<item>
<widget class="QComboBox" name="voiceComboBox">
<property name="styleSheet">
<string notr="true">background-color: rgb(30, 44, 62); border: 1px solid rgba(82, 194, 242, 0.3); border-radius: 3px; padding: 3px;</string>
</property>
<item>
<property name="text">
<string>标准女声</string>
</property>
</item>
<item>
<property name="text">
<string>标准男声</string>
</property>
</item>
<item>
<property name="text">
<string>儿童声</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="generateTtsButton">
<property name="text">
<string>🎵 生成语音</string>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
background-color: rgb(85, 125, 165);
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: rgb(105, 145, 185);
}
QPushButton:pressed {
background-color: rgb(95, 135, 175);
}</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="playTtsButton">
<property name="text">
<string>▶ 试听TTS</string>
</property>
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="ttsStatusLabel">
<property name="text">
<string>TTS状态: 就绪</string>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(160, 170, 180); font-size: 12px;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="orLabel2">
<property name="text">
<string>或者</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(160, 170, 180); font-size: 12px; margin: 10px;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="uploadLabel">
<property name="text">
<string>📁 上传文件:</string>
</property>
<property name="styleSheet">
<string notr="true">font-size: 14px; font-weight: bold; color: rgb(82, 194, 242);</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="uploadFileLayout">
<item>
<widget class="QLineEdit" name="filePathLineEdit">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="placeholderText">
<string>请选择一个.wav音频文件...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browseButton">
<property name="text">
<string>浏览...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="uploadAndPlayButton">
<property name="text">
<string>⬆️ 上传并播放</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="logLabel">
<property name="text">
<string>执行日志:</string>
</property>
<property name="styleSheet">
<string notr="true">font-size: 16px; font-weight: bold;</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="logTextEdit">
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>22</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>

@ -27,7 +27,8 @@ SOURCES += \
src/ui/main/MainWindow.cpp \
src/ui/dialogs/DeviceDialog.cpp \
src/ui/components/DeviceCard.cpp \
src/ui/components/DeviceListPanel.cpp
src/ui/components/DeviceListPanel.cpp \
AudioModule/IntelligenceUI.cpp
# Header files - 按模块组织
HEADERS += \
@ -36,12 +37,14 @@ HEADERS += \
include/ui/main/MainWindow.h \
include/ui/dialogs/DeviceDialog.h \
include/ui/components/DeviceCard.h \
include/ui/components/DeviceListPanel.h
include/ui/components/DeviceListPanel.h \
AudioModule/IntelligenceUI.h
# UI forms - 按模块组织
FORMS += \
forms/main/MainWindow.ui \
forms/dialogs/DeviceDialog.ui
forms/dialogs/DeviceDialog.ui \
AudioModule/IntelligenceUI.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin

@ -38,7 +38,7 @@
#include <QPair>
// 自定义模块头文件
// #include "AudioModule/IntelligenceUI.h" // 暂时注释掉,待实现
#include "AudioModule/IntelligenceUI.h" // 暂时注释掉,待实现
#include "ui/components/DeviceListPanel.h"
// 标准库头文件
@ -244,7 +244,7 @@ private:
private:
Ui::MainWindow *m_ui; ///< UI界面指针
// IntelligenceUI *m_intelligenceUI; ///< 情报传达界面指针(暂时注释掉)
IntelligenceUI *m_intelligenceUI; ///< 情报传达界面指针(暂时注释掉)
DeviceListPanel *m_deviceListPanel; ///< 设备列表面板组件
QVector<QPair<QString, QString>> m_robotList; ///< 机器人列表(名称-IP地址对
QVector<QPair<QString, QString>> m_uavList; ///< 无人机列表(名称-IP地址对

@ -8,5 +8,8 @@ unset LOCPATH
unset GIO_MODULE_DIR
unset GSETTINGS_SCHEMA_DIR
# 禁用硬件加速解决WSL下的OpenGL兼容性问题
export QT_XCB_GL_INTEGRATION=none
# 启动程序
exec ./BattlefieldExplorationSystem "$@"

@ -44,7 +44,7 @@ MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_ui(new Ui::MainWindow)
, m_deviceListPanel(nullptr)
// , m_intelligenceUI(nullptr) // 暂时注释掉
, m_intelligenceUI(nullptr)
{
m_ui->setupUi(this);
@ -81,8 +81,24 @@ void MainWindow::setupUI()
// 初始化随机数生成器
qsrand(QTime::currentTime().msec());
// 创建并集成DeviceListPanel到左侧面板
setupDeviceListPanel();
// 右侧情报与控制面板
QVBoxLayout *rightLayout = new QVBoxLayout();
rightLayout->setContentsMargins(10, 10, 10, 10);
rightLayout->setSpacing(15);
m_ui->rightPanel->setLayout(rightLayout);
// 添加情报传达模块
m_intelligenceUI = new IntelligenceUI(this);
rightLayout->addWidget(m_intelligenceUI);
// 初始化并添加设备列表面板
m_deviceListPanel = new DeviceListPanel(this);
rightLayout->addWidget(m_deviceListPanel);
rightLayout->addStretch(); // 添加伸缩,确保内容靠上
// 初始化数据库
DogDatabase::getInstance();
// 恢复地图显示控制
setupMapDisplay();
@ -510,16 +526,13 @@ void MainWindow::onSmartNavigationClicked()
void MainWindow::onIntelligenceClicked()
{
// 暂时注释掉 IntelligenceUI 相关功能,待实现
/*
// 恢复 IntelligenceUI 相关功能
if (!m_intelligenceUI) {
m_intelligenceUI = new IntelligenceUI(this);
}
m_intelligenceUI->show();
m_intelligenceUI->activateWindow();
m_intelligenceUI->raise();
*/
qDebug() << "Intelligence UI feature not implemented yet";
}
void MainWindow::onDeviceSelected(const QString &deviceId)

Loading…
Cancel
Save