diff --git a/README.md b/README.md index 51093e2f..e3f64331 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # Software_Architecture +requirement: +sshpass +安装espeak-ng for TTS引擎 \ No newline at end of file diff --git a/src/Client/AudioModule/IntelligenceUI.cpp b/src/Client/AudioModule/IntelligenceUI.cpp new file mode 100644 index 00000000..ad5edd92 --- /dev/null +++ b/src/Client/AudioModule/IntelligenceUI.cpp @@ -0,0 +1,694 @@ +#include "IntelligenceUI.h" +#include "ui_IntelligenceUI.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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("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::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::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::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::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::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::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); +} \ No newline at end of file diff --git a/src/Client/AudioModule/IntelligenceUI.cppZone.Identifier b/src/Client/AudioModule/IntelligenceUI.cppZone.Identifier new file mode 100644 index 00000000..f46bcc49 --- /dev/null +++ b/src/Client/AudioModule/IntelligenceUI.cppZone.Identifier @@ -0,0 +1,2 @@ +[ZoneTransfer] +ZoneId=3 diff --git a/src/Client/AudioModule/IntelligenceUI.h b/src/Client/AudioModule/IntelligenceUI.h new file mode 100644 index 00000000..4748619c --- /dev/null +++ b/src/Client/AudioModule/IntelligenceUI.h @@ -0,0 +1,121 @@ +#ifndef INTELLIGENCEUI_H +#define INTELLIGENCEUI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 \ No newline at end of file diff --git a/src/Client/AudioModule/IntelligenceUI.hZone.Identifier b/src/Client/AudioModule/IntelligenceUI.hZone.Identifier new file mode 100644 index 00000000..f46bcc49 --- /dev/null +++ b/src/Client/AudioModule/IntelligenceUI.hZone.Identifier @@ -0,0 +1,2 @@ +[ZoneTransfer] +ZoneId=3 diff --git a/src/Client/AudioModule/IntelligenceUI.ui b/src/Client/AudioModule/IntelligenceUI.ui new file mode 100644 index 00000000..e2a686f1 --- /dev/null +++ b/src/Client/AudioModule/IntelligenceUI.ui @@ -0,0 +1,634 @@ + + + IntelligenceUI + + + + 0 + 0 + 900 + 800 + + + + 情报传达系统 - UnitreeGo1 + + + 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; +} + + + + + 20 + + + 30 + + + 20 + + + 30 + + + 20 + + + + + 🔊 情报传达系统 + + + Qt::AlignCenter + + + 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; +} + + + + + + + SSH 连接设置 + + + 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; +} + + + + + + + + 目标 IP: + + + + + + + 192.168.123.13 + + + + + + + 用户名: + + + + + + + unitree + + + + + + + 密码: + + + + + + + 123 + + + QLineEdit::Password + + + + + + + + + 保存并应用设置 + + + + + + + + + + 当前目标: 192.168.123.13 (UnitreeGo1) + + + Qt::AlignCenter + + + color: rgb(160, 170, 180); font-size: 16px; + + + + + + + + + 选择音频文件: + + + font-size: 16px; font-weight: bold; + + + + + + + + 200 + 0 + + + + + warning.wav + + + + + alert.wav + + + + + emergency.wav + + + + + notification.wav + + + + + + + + 刷新列表 + + + + + + + + + + + 🔊 播放选定音频 + + + 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); +} + + + + + + + 🔧 解除音频占用 + + + 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); +} + + + + + + + + + 自定义音频制作 + + + + + + 🎤 录制语音: + + + font-size: 14px; font-weight: bold; color: rgb(82, 194, 242); + + + + + + + + + 🎤 开始录制 + + + 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); +} + + + + + + + ⏹ 停止录制 + + + false + + + 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); +} + + + + + + + ▶ 试听录音 + + + false + + + + + + + + + 录制状态: 就绪 + + + color: rgb(160, 170, 180); font-size: 12px; + + + + + + + 100 + + + 0 + + + false + + + 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; +} + + + + + + + 或者 + + + Qt::AlignCenter + + + color: rgb(160, 170, 180); font-size: 12px; margin: 10px; + + + + + + + 🗣 文字转语音: + + + font-size: 14px; font-weight: bold; color: rgb(82, 194, 242); + + + + + + + + 0 + 80 + + + + 输入要转换为语音的文字内容... + + + background-color: rgb(30, 44, 62); border: 1px solid rgba(82, 194, 242, 0.3); border-radius: 3px; padding: 5px; + + + + + + + + + background-color: rgb(30, 44, 62); border: 1px solid rgba(82, 194, 242, 0.3); border-radius: 3px; padding: 3px; + + + + 标准女声 + + + + + 标准男声 + + + + + 儿童声 + + + + + + + + 🎵 生成语音 + + + 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); +} + + + + + + + ▶ 试听TTS + + + false + + + + + + + + + TTS状态: 就绪 + + + color: rgb(160, 170, 180); font-size: 12px; + + + + + + + 或者 + + + Qt::AlignCenter + + + color: rgb(160, 170, 180); font-size: 12px; margin: 10px; + + + + + + + 📁 上传文件: + + + font-size: 14px; font-weight: bold; color: rgb(82, 194, 242); + + + + + + + + + true + + + 请选择一个.wav音频文件... + + + + + + + 浏览... + + + + + + + + + ⬆️ 上传并播放 + + + + + + + + + + 0 + + + true + + + + + + + 执行日志: + + + font-size: 16px; font-weight: bold; + + + + + + + + 0 + 200 + + + + true + + + + + + + + + 0 + 0 + 900 + 22 + + + + + + + + \ No newline at end of file diff --git a/src/Client/AudioModule/IntelligenceUI.uiZone.Identifier b/src/Client/AudioModule/IntelligenceUI.uiZone.Identifier new file mode 100644 index 00000000..f46bcc49 --- /dev/null +++ b/src/Client/AudioModule/IntelligenceUI.uiZone.Identifier @@ -0,0 +1,2 @@ +[ZoneTransfer] +ZoneId=3 diff --git a/src/Client/BattlefieldExplorationSystem b/src/Client/BattlefieldExplorationSystem index 38c0c0f0..32ecf1d7 100755 Binary files a/src/Client/BattlefieldExplorationSystem and b/src/Client/BattlefieldExplorationSystem differ diff --git a/src/Client/BattlefieldExplorationSystem.pro b/src/Client/BattlefieldExplorationSystem.pro index 29f16305..2fb2e366 100644 --- a/src/Client/BattlefieldExplorationSystem.pro +++ b/src/Client/BattlefieldExplorationSystem.pro @@ -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 diff --git a/src/Client/include/ui/main/MainWindow.h b/src/Client/include/ui/main/MainWindow.h index 9c40e660..bd3e75ea 100644 --- a/src/Client/include/ui/main/MainWindow.h +++ b/src/Client/include/ui/main/MainWindow.h @@ -38,7 +38,7 @@ #include // 自定义模块头文件 -// #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> m_robotList; ///< 机器人列表(名称-IP地址对) QVector> m_uavList; ///< 无人机列表(名称-IP地址对) diff --git a/src/Client/recordings/recorded_20250620_202850.wav b/src/Client/recordings/recorded_20250620_202850.wav new file mode 100644 index 00000000..4d97327c Binary files /dev/null and b/src/Client/recordings/recorded_20250620_202850.wav differ diff --git a/src/Client/recordings/recorded_20250620_202911.wav b/src/Client/recordings/recorded_20250620_202911.wav new file mode 100644 index 00000000..4d97327c Binary files /dev/null and b/src/Client/recordings/recorded_20250620_202911.wav differ diff --git a/src/Client/run_app.sh b/src/Client/run_app.sh index 9b4ce910..0ac3b901 100644 --- a/src/Client/run_app.sh +++ b/src/Client/run_app.sh @@ -8,5 +8,8 @@ unset LOCPATH unset GIO_MODULE_DIR unset GSETTINGS_SCHEMA_DIR +# 禁用硬件加速,解决WSL下的OpenGL兼容性问题 +export QT_XCB_GL_INTEGRATION=none + # 启动程序 exec ./BattlefieldExplorationSystem "$@" diff --git a/src/Client/src/ui/main/MainWindow.cpp b/src/Client/src/ui/main/MainWindow.cpp index 38078ed1..5acf3881 100644 --- a/src/Client/src/ui/main/MainWindow.cpp +++ b/src/Client/src/ui/main/MainWindow.cpp @@ -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) diff --git a/src/Client/tts_output/tts_20250620_213129.wav b/src/Client/tts_output/tts_20250620_213129.wav new file mode 100644 index 00000000..6cd3af0d Binary files /dev/null and b/src/Client/tts_output/tts_20250620_213129.wav differ diff --git a/src/Client/tts_output/tts_20250620_213147.wav b/src/Client/tts_output/tts_20250620_213147.wav new file mode 100644 index 00000000..6cd3af0d Binary files /dev/null and b/src/Client/tts_output/tts_20250620_213147.wav differ diff --git a/src/Client/tts_output/tts_20250620_213215.wav b/src/Client/tts_output/tts_20250620_213215.wav new file mode 100644 index 00000000..c81d57b7 Binary files /dev/null and b/src/Client/tts_output/tts_20250620_213215.wav differ