活动介绍

#include "mainwindow.h" #include "ui_mainwindow.h" #include<QMessageBox> #include<QTextEdit> #include<QPushButton> TemperatureThread::TemperatureThread(QObject *parent) : QThread(parent) { m_temperature = 0.0; QPushButton * btn= new QPushButton(); //btn->show(); btn->setText("this"); btn->setParent(QObject); } void TemperatureThread::run() { while (true){ m_temperature = qrand() % 100; emit temperatureChanged(m_temperature); msleep(1000); } } MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); m_temperatureThread = new TemperatureThread(this); m_timer = new QTimer(this); m_threshold = 0.0; connect(m_temperatureThread, &TemperatureThread::temperatureChanged, this,&MainWindow::updateTemperature); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_startButton_clicked() { if (m_timer->isActive()) { QMessageBox::warning(this,tr("Warning"),tr("Alarm already started")); return; } bool ok; m_threshold = ui->thresholdEdit->text().toDouble(&ok); if (!ok) { QMessageBox::warning(this,tr("warning"),tr("Invalid")); return; } m_temperatureThread->start(); m_timer->start(1000); } void MainWindow::on_stopButton_clicked() { if (!m_timer->isActive()){ QMessageBox::warning(this,tr("Warning"),tr("Alarm not started yet")); return; } m_temperatureThread->quit(); m_temperatureThread->wait(); m_timer->stop(); ui->temperatureLabel->setText(tr("0.0")); } void MainWindow::updateTemperature(double temperature) { ui->temperatureLabel->setText(QString::number(temperature)); if (temperature > m_threshold){ QMessageBox::critical(this,tr("Warning"),tr("Temperature too high")); } }错在哪

时间: 2023-12-03 12:46:33 浏览: 172
代码存在一些问题: 1. 在 TemperatureThread 的构造函数中创建了一个 QPushButton 实例,但并没有将其添加到界面中。 2. 在创建 QPushButton 实例时,使用了 QObject 作为其父对象,应该使用 MainWindow 或其子类作为父对象。 3. 在 on_startButton_clicked 函数中,应该先判断是否已经启动了温度线程,再进行阈值的设置和启动定时器。 4. 在 on_stopButton_clicked 函数中,应该先判断是否已经启动了定时器和温度线程,再停止定时器和温度线程,并将温度显示标签的文本清零。 5. 在 updateTemperature 函数中,应该将温度值转换为字符串后再设置到温度显示标签上。 下面是修改后的代码: ```c++ #include "mainwindow.h" #include "ui_mainwindow.h" #include <QMessageBox> #include <QTextEdit> #include <QPushButton> TemperatureThread::TemperatureThread(QObject *parent) : QThread(parent) { m_temperature = 0.0; } void TemperatureThread::run() { while (true){ m_temperature = qrand() % 100; emit temperatureChanged(m_temperature); msleep(1000); } } MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); m_temperatureThread = new TemperatureThread(this); m_timer = new QTimer(this); m_threshold = 0.0; connect(m_temperatureThread, &TemperatureThread::temperatureChanged, this, &MainWindow::updateTemperature); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_startButton_clicked() { if (m_temperatureThread->isRunning()) { QMessageBox::warning(this,tr("Warning"),tr("Alarm already started")); return; } bool ok; m_threshold = ui->thresholdEdit->text().toDouble(&ok); if (!ok) { QMessageBox::warning(this,tr("warning"),tr("Invalid")); return; } m_temperatureThread->start(); m_timer->start(1000); } void MainWindow::on_stopButton_clicked() { if (!m_timer->isActive() || !m_temperatureThread->isRunning()) { QMessageBox::warning(this,tr("Warning"),tr("Alarm not started yet")); return; } m_temperatureThread->quit(); m_temperatureThread->wait(); m_timer->stop(); ui->temperatureLabel->setText(QString::number(0.0)); } void MainWindow::updateTemperature(double temperature) { ui->temperatureLabel->setText(QString::number(temperature)); if (temperature > m_threshold){ QMessageBox::critical(this,tr("Warning"),tr("Temperature too high")); } } ```
阅读全文

相关推荐

#include "mainwindow.h"#include "ui_mainwindow.h"#include <QMessageBox>#include <QTextEdit>#include <QPushButton>TemperatureThread::TemperatureThread(QObject *parent) : QThread(parent){ m_temperature = 0.0;}void TemperatureThread::run(){ while (true){ m_temperature = qrand() % 100; emit temperatureChanged(m_temperature); msleep(1000); }}MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow){ ui->setupUi(this); m_temperatureThread = new TemperatureThread(this); m_timer = new QTimer(this); m_threshold = 0.0; connect(m_temperatureThread, &TemperatureThread::temperatureChanged, this, &MainWindow::updateTemperature);}MainWindow::~MainWindow(){ delete ui;}void MainWindow::on_startButton_clicked(){ if (m_temperatureThread->isRunning()) { QMessageBox::warning(this,tr("Warning"),tr("Alarm already started")); return; } bool ok; m_threshold = ui->thresholdEdit->text().toDouble(&ok); if (!ok) { QMessageBox::warning(this,tr("warning"),tr("Invalid")); return; } m_temperatureThread->start(); m_timer->start(1000);}void MainWindow::on_stopButton_clicked(){ if (!m_timer->isActive() || !m_temperatureThread->isRunning()) { QMessageBox::warning(this,tr("Warning"),tr("Alarm not started yet")); return; } m_temperatureThread->quit(); m_temperatureThread->wait(); m_timer->stop(); ui->temperatureLabel->setText(QString::number(0.0));}void MainWindow::updateTemperature(double temperature){ ui->temperatureLabel->setText(QString::number(temperature)); if (temperature > m_threshold){ QMessageBox::critical(this,tr("Warning"),tr("Temperature too high")); }}这段代码怎么改,可以使timer的数据逐渐增大,而不是随意乱弹出数据

#include "ui_mainwindow.h" #include "serialui.h" #include <QSerialPortInfo> SerialUI::SerialUI(QWidget *parent) : QWidget(parent), ui(new Ui::MainWindow) { ui->setupUi(this); initPortControls(); // 连接按钮信号 connect(ui->openButton, &QPushButton::clicked, this, &SerialUI::onOpenClicked); connect(ui->sendButton, &QPushButton::clicked, [=]{ emit dataToSend(ui->sendEdit->text().toUtf8()); }); } void SerialUI::initPortControls() { // 填充可用端口 ui->portCombo->clear(); foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { ui->portCombo->addItem(info.portName()); } // 波特率选项 ui->baudCombo->addItems({"9600", "19200", "38400", "57600", "115200"}); // 数据位 ui->dataBitsCombo->addItem("8", QSerialPort::Data8); ui->dataBitsCombo->setCurrentIndex(0); // 校验位 ui->parityCombo->addItem("无", QSerialPort::NoParity); ui->parityCombo->addItem("奇校验", QSerialPort::OddParity); ui->parityCombo->addItem("偶校验", QSerialPort::EvenParity); } void SerialUI::onOpenClicked() { SerialParamInfo config; config.portName = ui->portCombo->currentText(); config.baudRate = ui->baudCombo->currentText().toInt(); config.dataBits = static_cast<QSerialPort::DataBits>( ui->dataBitsCombo->currentData().toInt()); config.parity = static_cast<QSerialPort::Parity>( ui->parityCombo->currentData().toInt()); emit openRequested(config); } QString SerialUI::getSelectedPort() const { return ui->portCombo->currentText(); }// ui_mainwindow.h (由Qt uic工具自动生成,不要手动修改) namespace Ui { class MainWindow; } // 自定义UI管理类(可选) class SerialUI : public QWidget { Q_OBJECT public: explicit SerialUI(QWidget *parent = nullptr); void initPortControls(); // 初始化串口参数控件 QString getSelectedPort() const; // 获取当前选择的端口 signals: void openRequested(const SerialParamInfo &config); void dataToSend(const QByteArray &data); priva

#include "mainwindow.h" #include "ui_mainwindow.h" #include <QHBoxLayout> #include <QVBoxLayout> #include <QMessageBox> #include <QGroupBox> #include <QFormLayout> #include <QPixmap> #include <QSerialPortInfo> #include <QDateTime> #include <windows.h> #include <dwmapi.h> #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_serialWorker(nullptr) , timeoutTimer(new QTimer(this)) , currentActiveButton(nullptr) , workModeButton(nullptr) , m_isHandshakeSuccess(false) , sensorVoltageTimer(new QTimer(this)) , isGettingSensorVoltage(false) , m_isCISChecking(false) , m_agingTimer(new QTimer(this)) { this->setStyleSheet("QMainWindow{background-color:#F6F6F2;}"); if(HWND hwnd = (HWND)winId()) { BOOL darkMode = FALSE; DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &darkMode, sizeof(darkMode)); } ui->setupUi(this); setWindowTitle(tr("点钞机")); setMinimumSize(800, 600); //主布局-左右分割1:3 mainLayout = new QHBoxLayout(); mainLayout->setSpacing(10); mainLayout->setStretch(0, 1); //左侧区域 mainLayout->setStretch(1, 2); //中间区域 mainLayout->setStretch(2, 1); //右侧区域 //========== 左侧区域 ========== QWidget *leftWidget = new QWidget(); QVBoxLayout *leftLayout = new QVBoxLayout(leftWidget); leftLayout->setSpacing(10); QGroupBox *portGroup = new QGroupBox(tr("串口设置")); portGroup->setStyleSheet("QGroupBox { background-color: #F6F6F2; border: 1px solid #ccc; border-radius: 5px; margin-top: 10px;color:#388087; font-weight: bold; }" "QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; background-color:#F6F6F2;font-weight: bold; }"); QVBoxLayout *portLayout = new QVBoxLayout(portGroup); portComboBox = new QComboBox(portGroup); portComboBox->setStyleSheet("background-color:#ebebe7;color:#388087;font-weight: bold; "); //波特率选择下拉框 baudRateComboBox = new QComboBox(portGroup); baudRateComboBox->setStyleSheet("background-color:#ebebe7;color:#388087;font-weight: bold; "); baudRateComboBox->addItem("115200", QSerialPort::Baud115200); baudRateComboBox->addItem("9600", QSerialPort::Baud9600); baudRateComboBox->setCurrentIndex(0); //默认115200 currentBaudRate = QSerialPort::Baud115200; //连接波特率变化信号 connect(baudRateComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::onBaudRateChanged); btnRefreshPorts = new QPushButton("刷新串口", portGroup); btnRefreshPorts->setStyleSheet("QPushButton {" "background-color:#e0e0e0;" "color:#388087;" "font-weight: bold;" "}" "QPushButton:hover{""background-color: #e0e0e0;color:#388087;" "}" "QPushButton:pressed {" "background-color: #a0a0a0;" "color:#388087;" "}"); btnConnect = new QPushButton("连接串口", portGroup); btnConnect->setStyleSheet("QPushButton {" "background-color:#e0e0e0;" "color:#388087;" "font-weight: bold;" "}" "QPushButton:hover{""background-color: #e0e0e0;color:#388087;" "}" "QPushButton:pressed {" "background-color: #a0a0a0;" "color:#388087;" "}"); portLayout->addWidget(portComboBox); portLayout->addWidget(baudRateComboBox); //添加波特率下拉框 portLayout->addWidget(btnRefreshPorts); portLayout->addWidget(btnConnect); portLayout->setStretch(0, 1); portLayout->setStretch(1, 1); portLayout->setStretch(2, 1); portLayout->setStretch(3, 1); leftLayout->addWidget(portGroup,2); QGroupBox *infoGroup = new QGroupBox("机器信息"); infoGroup->setStyleSheet("background-color:#F6F6F2;color:#388087;border: 1px solid #ccc;padding: 5px;font-weight: bold; "); QVBoxLayout *infoLayout = new QVBoxLayout(infoGroup); machineInfoLineEdit = new QTextEdit(); machineInfoLineEdit->setReadOnly(true); machineInfoLineEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QFont font; font.setPointSize(12); machineInfoLineEdit->setFont(font); //设置行间距 machineInfoLineEdit->setStyleSheet( "QTextEdit {" " line-height: 150%;" " background-color: #F6F6F2;" " border: 1px solid #ccc;" " padding: 5px;" " color: black;" " font-weight: bold; " "}" ); machineInfoLineEdit->setLineWrapMode(QTextEdit::WidgetWidth); machineInfoLineEdit->setWordWrapMode(QTextOption::WrapAnywhere); //machineInfoLineEdit->setMinimumHeight(200); machineInfoLineEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); infoLayout->addWidget(machineInfoLineEdit); leftLayout->addWidget(infoGroup, 3); //-------------计数显示区域-------------- CountingInfoWidget *countingWidget = new CountingInfoWidget(); QVBoxLayout *countLayout = new QVBoxLayout(countingWidget); countLayout->setSpacing(10); countLayout->setContentsMargins(10, 10, 10, 10); countLayout->setStretch(0, 3); //表格区域 countLayout->setStretch(1, 1); //按钮区域 //创建并配置表格 denominationTable = new QTableWidget(countingWidget); denominationTable->setObjectName("denominationTable"); denominationTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); denominationTable->setColumnCount(3); //金额、数量、单位 denominationTable->setRowCount(0); denominationTable->verticalHeader()->setVisible(false); //隐藏行头 denominationTable->horizontalHeader()->setVisible(false); //隐藏列头 denominationTable->setShowGrid(false); //隐藏网格线 denominationTable->setEditTriggers(QAbstractItemView::NoEditTriggers); //禁止编辑 denominationTable->setSelectionMode(QAbstractItemView::NoSelection); //禁止选择 denominationTable->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); //固定大小 //设置表格样式 denominationTable->setStyleSheet( "QTableWidget {" " background-color: transparent;" " border: none;" " color: #388087;" "}" "QTableWidget::item {" " padding: 2px;" " border: none;" "}" ); //设置列宽 denominationTable->setColumnWidth(0, 80); //金额列 denominationTable->setColumnWidth(1, 40); //数量列 denominationTable->setColumnWidth(2, 40); //单位列 //将表格添加到布局 //countLayout->addWidget(denominationTable, 3, Qt::AlignLeft | Qt::AlignTop); connect(countingWidget, &CountingInfoWidget::blankAreaClicked, this, &MainWindow::onAmountLabelClicked); //主布局:上部显示区域和下部按钮 QHBoxLayout *mainCountLayout = new QHBoxLayout(); //左侧布局 - 退钞数 QVBoxLayout *refundLayout = new QVBoxLayout(); QLabel* refundTitleLabel = new QLabel("退钞数"); //refundTitleLabel->setFixedWidth(120); refundTitleLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); refundTitleLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;font:14px;border: none;font-weight: bold; "); refundTitleLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); refundLabel = new ClickableLabel(); // refundLabel->setFixedWidth(90); refundLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); refundLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;padding: 5px;border: none;font:bold 17px;font-weight: bold; "); refundLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); //添加SN显示label QHBoxLayout *refundRowLayout = new QHBoxLayout(); //refundRowLayout->addWidget(refundLabel); snLabel = new QLabel(); snLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;padding: 5px;border: none;font:bold 12px;font-weight: bold; "); snLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); snLabel->setMinimumWidth(150); //为SN留出足够空间 refundRowLayout->addWidget(refundTitleLabel); refundRowLayout->addWidget(snLabel); refundLayout->addWidget(denominationTable, 1); refundLayout->addLayout(refundRowLayout,0); refundLayout->addWidget(refundLabel,0); //使用水平布局替代直接添加refundLabel //中间布局-弹性空间 QSpacerItem *spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); //右侧布局-包含金额和张数 QVBoxLayout *rightcountLayout = new QVBoxLayout(); //金额部分 QVBoxLayout *amountLayout = new QVBoxLayout(); QLabel* amountTitleLabel = new QLabel("金额"); amountTitleLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); amountTitleLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;font:14px;border: none;font-weight: bold; "); amountTitleLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); amountLabel = new ClickableLabel(); amountLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;padding: 5px;border: none;font:bold 17px;font-weight: bold; "); amountLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); amountLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); amountLayout->addStretch(); //弹性空间使金额靠右 amountLayout->addWidget(amountTitleLabel); amountLayout->addWidget(amountLabel); rightcountLayout->addLayout(amountLayout); rightcountLayout->addStretch(); //张数部分 QVBoxLayout *sheetLayout = new QVBoxLayout(); QLabel* sheetTitleLabel = new QLabel("张数"); sheetTitleLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;font:14px;border: none;font-weight: bold; "); sheetTitleLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); sheetNumberLabel = new ClickableLabel(); sheetNumberLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;padding: 5px;border: none;font:bold 17px;font-weight: bold; "); sheetNumberLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); sheetTitleLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); sheetNumberLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); sheetLayout->addStretch(); //弹性空间使张数靠右 sheetLayout->addWidget(sheetTitleLabel); sheetLayout->addWidget(sheetNumberLabel); rightcountLayout->addLayout(sheetLayout); refundLabel->setText("0"); amountLabel->setText("0"); sheetNumberLabel->setText("0"); //设置标签可点击 refundLabel->setCursor(Qt::PointingHandCursor); amountLabel->setCursor(Qt::PointingHandCursor); sheetNumberLabel->setCursor(Qt::PointingHandCursor); //将各部分添加到主布局 mainCountLayout->addLayout(refundLayout,1); mainCountLayout->addItem(spacer); mainCountLayout->addLayout(rightcountLayout,1); //连接点击信号 connect(refundLabel, &ClickableLabel::clicked, this, &MainWindow::onRefundLabelClicked); connect(amountLabel, &ClickableLabel::clicked, this, &MainWindow::onAmountLabelClicked); connect(sheetNumberLabel, &ClickableLabel::clicked, this, &MainWindow::onSheetNumberLabelClicked); //将主显示布局添加到计数组 countLayout->addLayout(mainCountLayout,2); buttonStyle = "QPushButton {" "background-color: #f8f8f8;" "border: 1px solid #ccc;" "border-radius: 5px;" "padding: 8px;" "width: 120px;" "height: 50px;" "color:#388087;" "font-weight: bold; " "}" "QPushButton:hover {" "background-color: #f0f0f0;" "color:#388087;" "font-weight: bold; " "}" "QPushButton:pressed {" "background-color: #c0c0c0;" "color:#388087;" "font-weight: bold; " "}"; //添加重置按钮 resetCountButton = new QPushButton("重置计数"); resetCountButton->setStyleSheet(buttonStyle); resetCountButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); connect(resetCountButton, &QPushButton::clicked, this,&MainWindow::on_resetCountButton_clicked); countLayout->addWidget(resetCountButton); leftLayout->addWidget(countingWidget, 3); mainLayout->addWidget(leftWidget, 1); //========== 中间区域 ========== QWidget *centerWidget = new QWidget(); QVBoxLayout *centerLayout = new QVBoxLayout(centerWidget); centerLayout->setSpacing(10); //机器状态区域 QGroupBox *statusGroup = new QGroupBox("机器状态"); statusGroup->setStyleSheet("QGroupBox { background-color: #F6F6F2; border: 1px solid #ccc; border-radius: 5px; margin-top: 10px;color:#388087; font-weight: bold; }" "QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; background-color:#F6F6F2;font-weight: bold; }"); QHBoxLayout *statusLayout = new QHBoxLayout(statusGroup); machineStatusImage = new QLabel(); machineStatusImage->setAlignment(Qt::AlignCenter); machineStatusImage->setScaledContents(true); machineStatusImage->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // QLabel* overlayLabel = new QLabel(machineStatusImage); // overlayLabel->setStyleSheet("background-color: transparent; color: red; font-weight: bold;"); // overlayLabel->setText("QT"); // overlayLabel->move(250, 110); // 设置位置 // overlayLabel->adjustSize(); // overlayLabel->setAttribute(Qt::WA_TransparentForMouseEvents); // 允许点击穿透 voltageTable = new QTableWidget(); voltageTable->setColumnCount(3); //voltageTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); voltageTable->setHorizontalHeaderLabels({"传感器ID", "传感器名称", "电压值(V)"}); voltageTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); voltageTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); voltageTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); voltageTable->setStyleSheet( "QTableWidget {" " background-color: #F6F6F2;" " border: 1px solid #ccc;" " color: #388087;" "}" "QTableWidget::item {" " padding: 2px;" " border: none;" " background-color: #F6F6F2;" " color: #388087;" "}" "QTableWidget::item:selected {" " background-color: #F6F6F2;" " color: #388087;" "}" "QHeaderView {" " background-color: #F6F6F2;" //表头整体背景色 "}" "QScrollBar:vertical {" " background: #F6F6F2;" " width: 12px;" " margin: 0px 0px 0px 0px;" "}" "QHeaderView::section {" " background-color: #F6F6F2;" " padding: 4px;" " border: 1px solid #ccc;" " color: #388087;" "}" "QTableCornerButton::section {" " background-color: #F6F6F2;" //左上角空白区域 " border: 1px solid #ccc;" "}" ); voltageTable->setVisible(false); //初始隐藏表格 QPixmap pixmap(":/image/333.svg"); if(!pixmap.isNull()) { machineStatusImage->setMaximumSize(450, 700); QPixmap scaled = pixmap.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); machineStatusImage->setPixmap(scaled); } else { machineStatusImage->setText("图片加载失败"); } statusLayout->addWidget(machineStatusImage, 3); statusLayout->addWidget(voltageTable, 1); centerLayout->addWidget(statusGroup, 3); //信息显示区域 QGroupBox *messageGroup = new QGroupBox(); messageGroup->setStyleSheet("background-color:#F6F6F2;color:#388087;border: 1px solid #ccc;padding: 5px;font-weight: bold; "); QVBoxLayout *messageLayout = new QVBoxLayout(messageGroup); //添加顶部布局,包含标题和清除按钮 QHBoxLayout *messageHeaderLayout = new QHBoxLayout(); QLabel *messageTitleLabel = new QLabel("信息显示"); messageTitleLabel->setStyleSheet("background-color:#F6F6F2;color:#388087;font-weight: bold;"); QPushButton *clearButton = new QPushButton("清空"); clearButton->setStyleSheet("QPushButton {" "background-color: #f0f0f0;" "border: 1px solid #ccc;" "border-radius: 3px;" "padding: 3px 8px;" "color:#388087;" "font-weight: bold;" "}" "QPushButton:hover {" "background-color: #e0e0e0;" "}" "QPushButton:pressed {" "background-color: #a0a0a0;" "}"); clearButton->setFixedWidth(60); //设置固定宽度 messageHeaderLayout->addWidget(messageTitleLabel); messageHeaderLayout->addStretch(); messageHeaderLayout->addWidget(clearButton); messageLayout->addLayout(messageHeaderLayout); messageTextEdit = new QTextEdit(); messageTextEdit->setReadOnly(true); messageTextEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); messageTextEdit->setStyleSheet("QTextEdit { background-color: #F6F6F2; border: 1px solid #ccc; padding: 5px; color:black;font-weight: bold;/* font-family:宋体; */font-size:14px;}"); messageLayout->addWidget(messageTextEdit); //连接清除按钮的信号 connect(clearButton, &QPushButton::clicked, this, [this](){ messageTextEdit->clear(); }); centerLayout->addWidget(messageGroup, 2); mainLayout->addWidget(centerWidget, 2); //========== 右侧区域 ========== QWidget *rightWidget = new QWidget(); QVBoxLayout *rightLayout = new QVBoxLayout(rightWidget); rightLayout->setSpacing(10); //按钮区域 QGroupBox *buttonGroup = new QGroupBox("操作命令"); buttonGroup->setStyleSheet("QGroupBox { background-color: #F6F6F2; border: 1px solid #ccc black; border-radius: 5px; margin-top: 10px;color:#388087; font-weight: bold; }" "QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; background-color:#F6F6F2;font-weight: bold; }"); QVBoxLayout *buttonLayout = new QVBoxLayout(buttonGroup); commandStack = new QStackedWidget(); commandStack->addWidget(createFirstCommandPage()); //第一页 commandStack->addWidget((createSecondCommandPage())); //第二页 //添加导航按钮 QHBoxLayout* navLayout = new QHBoxLayout(); addNavigationButtons(navLayout); buttonLayout->addLayout(navLayout); buttonLayout->addWidget(commandStack); rightLayout->addWidget(buttonGroup); mainLayout->addWidget(rightWidget, 1); //设置中心窗口 QWidget *centralWidget = new QWidget(); centralWidget->setLayout(mainLayout); setCentralWidget(centralWidget); // QToolBar* langToolBar = addToolBar(tr("语言")); // QAction* zhAction = langToolBar->addAction("中文"); // QAction* enAction = langToolBar->addAction("英语"); // connect(zhAction, &QAction::triggered, this, [this]() { // loadLanguage("zh_CN"); // }); // connect(enAction, &QAction::triggered, this, [this]() { // loadLanguage("en"); // }); //连接信号槽 connect(btnRefreshPorts, &QPushButton::clicked, this, &MainWindow::refreshPorts); connect(btnConnect, &QPushButton::clicked, this, &MainWindow::toggleConnection); //初始化UI状态 refreshPorts(); enableControls(false); initializeErrorMaps(); //初始化传感器名称映射 sensorNameMap["0X01"] = "QTH"; sensorNameMap["0X02"] = "QTL"; sensorNameMap["0X03"] = "PS1L"; sensorNameMap["0X04"] = "PS1M"; sensorNameMap["0X05"] = "PS1R"; sensorNameMap["0X06"] = "PS2L"; sensorNameMap["0X07"] = "PS2R"; sensorNameMap["0X08"] = "PS3L"; sensorNameMap["0X09"] = "PS3M"; sensorNameMap["0X0A"] = "PS3R"; sensorNameMap["0X0B"] = "ST"; sensorNameMap["0X0C"] = "STL"; sensorNameMap["0X0D"] = "STR"; sensorNameMap["0X0E"] = "STU"; sensorNameMap["0X0F"] = "PS4"; sensorNameMap["0X10"] = "UV1"; sensorNameMap["0X11"] = "UV2"; sensorNameMap["0X12"] = "DWP"; sensorNameMap["0X13"] = "RJH"; sensorNameMap["0X14"] = "RJL"; sensorNameMap["0X15"] = "BTU"; sensorNameMap["0X16"] = "BTD"; sensorNameMap["0X17"] = "CVU"; sensorNameMap["0X18"] = "CVD"; sensorNameMap["0X19"] = "UVM"; sensorNameMap["0X1A"] = "SAFE"; sensorNameMap["0X1B"] = "PS5"; sensorNameMap["0X1C"] = "TKL"; sensorNameMap["0X1D"] = "TKR"; sensorNameMap["0X1E"] = "QTLoc"; sensorNameMap["0X1F"] = "TDLoc"; sensorNameMap["0X20"] = "RJLoc"; sensorNameMap["0X21"] = "FL"; sensorNameMap["0X22"] = "Chn"; //老化定时器 connect(m_agingTimer, &QTimer::timeout, this, [this]() { m_agingSeconds++; updateAgingTimeDisplay(); }); } MainWindow::~MainWindow() { //清理串口工作线程 if(m_serialWorker) { m_serialWorker->stop(); //停止线程 m_serialWorker->wait(); delete m_serialWorker; m_serialWorker = nullptr; } delete ui; } //波特率改变处理 void MainWindow::onBaudRateChanged(int index) { currentBaudRate = baudRateComboBox->currentData().toInt(); //如果已连接,则更新波特率 if(m_serialWorker) { m_serialWorker->setBaudRate(currentBaudRate); logMessage(QString("波特率已更改为: %1").arg(baudRateComboBox->currentText())); } } void MainWindow::onRefundLabelClicked() { if(!m_serialWorker) { logMessage("串口未打开"); return; } //清空之前的退钞数据 m_rejectList.clear(); m_rejectReasons.clear(); if(m_rejectDialog && m_rejectDialog->isVisible()) { m_rejectDialog->close(); } //初始化对话框 initRejectDialog(); //发送获取退钞口信息列表命令 sendGetRejectListCommand(); setButtonActive(rejectListButton); } void MainWindow::onAmountLabelClicked() { if(!m_serialWorker) { logMessage("串口未打开"); return; } //清空之前的明细数据 m_detailList.clear(); initDetailDialog(); sendDetailedlistCommand(); setButtonActive(detailedlistButton); } void MainWindow::onSheetNumberLabelClicked() { onAmountLabelClicked(); } QWidget* MainWindow::createFirstCommandPage() { QWidget *page = new QWidget(); QGridLayout *grid = new QGridLayout(page); grid->setSpacing(10); grid->setContentsMargins(10, 10, 10, 10); buttonStyle = "QPushButton {" "background-color: #f8f8f8;" "border: 1px solid #ccc;" "border-radius: 5px;" "padding: 8px;" "width: 120px;" "height: 50px;" "color:#388087;" "font-weight: bold; " "}" "QPushButton:hover {" "background-color: #f0f0f0;" "color:#388087;" "font-weight: bold; " "}" "QPushButton:pressed {" "background-color: #c0c0c0;" "color:#388087;" "font-weight: bold; " "}"; QWidget *handshakeWidget = new QWidget(); QVBoxLayout *handshakeLayout = new QVBoxLayout(handshakeWidget); handshakeLayout->setContentsMargins(0, 0, 0, 0); handshakeLayout->setSpacing(2); QLabel *emptyLabel3 = new QLabel(); emptyLabel3->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel3->setAlignment(Qt::AlignLeft | Qt::AlignBottom); handshakeLayout->addWidget(emptyLabel3); QPushButton *handshakeButton = new QPushButton("握手命令"); handshakeButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); handshakeButton->setStyleSheet(buttonStyle); handshakeLayout->addWidget(handshakeButton); grid->addWidget(handshakeWidget, 0, 0); QWidget *testWidget = new QWidget(); QVBoxLayout *testLayout = new QVBoxLayout(testWidget); testLayout->setContentsMargins(0, 0, 0, 0); testLayout->setSpacing(2); QLabel *emptyLabel4 = new QLabel(); emptyLabel4->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel4->setAlignment(Qt::AlignLeft | Qt::AlignBottom); testLayout->addWidget(emptyLabel4); QPushButton *testButton = new QPushButton("获取软件版本信息"); testButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); testButton->setStyleSheet(buttonStyle); testLayout->addWidget(testButton); grid->addWidget(testWidget, 0, 1); //启动机器按钮 QWidget *startMachineWidget = new QWidget(); QVBoxLayout *startMachineLayout = new QVBoxLayout(startMachineWidget); startMachineLayout->setContentsMargins(0, 0, 0, 0); startMachineLayout->setSpacing(2); QLabel *startMachineLabel = new QLabel("机器控制"); startMachineLabel->setStyleSheet("color:#388087; font-weight: bold;"); startMachineLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); startMachineLayout->addWidget(startMachineLabel); startMachineButton = new QPushButton("启动机器"); startMachineButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); startMachineButton->setStyleSheet(buttonStyle); startMachineLayout->addWidget(startMachineButton); grid->addWidget(startMachineWidget, 1, 0); //设置手动/自动启动 QWidget *handleSelfWidget = new QWidget(); QVBoxLayout *handleSelfLayout = new QVBoxLayout(handleSelfWidget); handleSelfLayout->setContentsMargins(0, 0, 0, 0); handleSelfLayout->setSpacing(2); QLabel *handleSelfLabel = new QLabel("手动/自动启动"); handleSelfLabel->setStyleSheet("color:#388087; font-weight: bold;"); handleSelfLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); handleSelfLayout->addWidget(handleSelfLabel); handleSelfButton = new QPushButton("设置手动/自动启动"); handleSelfButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); handleSelfButton->setStyleSheet(buttonStyle); handleSelfLayout->addWidget(handleSelfButton); grid->addWidget(handleSelfWidget, 1, 1); //工作模式按钮 QWidget *workModeWidget = new QWidget(); QVBoxLayout *workModeLayout = new QVBoxLayout(workModeWidget); workModeLayout->setContentsMargins(0, 0, 0, 0); workModeLayout->setSpacing(2); QLabel *workModeLabel = new QLabel("工作模式"); workModeLabel->setStyleSheet("color:#388087; font-weight: bold;"); workModeLayout->addWidget(workModeLabel); workModeLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); workModeButton = new QPushButton("选择工作模式"); workModeButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); workModeButton->setStyleSheet(buttonStyle); // workModeButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); workModeLayout->addWidget(workModeButton); grid->addWidget(workModeWidget, 2, 0); QWidget *currencyWidget = new QWidget(); QVBoxLayout *currencyLayout = new QVBoxLayout(currencyWidget); currencyLayout->setContentsMargins(0, 0, 0, 0); currencyLayout->setSpacing(2); QLabel *currencyLabel = new QLabel("货币币种"); currencyLabel->setStyleSheet("color:#388087; font-weight: bold;"); currencyLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); currencyLayout->addWidget(currencyLabel); currencyButton = new QPushButton("获取货币币种"); currencyButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); currencyButton->setStyleSheet(buttonStyle); currencyLayout->addWidget(currencyButton); grid->addWidget(currencyWidget, 2, 1); //预置数按钮 QWidget *presetWidget = new QWidget(); QVBoxLayout *presetLayout = new QVBoxLayout(presetWidget); presetLayout->setContentsMargins(0, 0, 0, 0); presetLayout->setSpacing(2); QLabel *presetLabel = new QLabel("预置数"); presetLabel->setStyleSheet("color:#388087; font-weight: bold;"); presetLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); presetLayout->addWidget(presetLabel); presetButton = new QPushButton("设置预置数"); presetButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); presetButton->setStyleSheet(buttonStyle); presetLayout->addWidget(presetButton); grid->addWidget(presetWidget, 3, 0); //预置金额按钮 QWidget *presetAmountWidget = new QWidget(); QVBoxLayout *presetAmountLayout = new QVBoxLayout(presetAmountWidget); presetAmountLayout->setContentsMargins(0, 0, 0, 0); presetAmountLayout->setSpacing(2); QLabel *presetAmountLabel = new QLabel("预置金额"); presetAmountLabel->setStyleSheet("color:#388087; font-weight: bold;"); presetAmountLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); presetAmountLayout->addWidget(presetAmountLabel); presetAmountButton = new QPushButton("设置预置金额"); presetAmountButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); presetAmountButton->setStyleSheet(buttonStyle); presetAmountLayout->addWidget(presetAmountButton); grid->addWidget(presetAmountWidget, 3, 1); //走钞速度按钮 QWidget *speedWidget = new QWidget(); QVBoxLayout *speedLayout = new QVBoxLayout(speedWidget); speedLayout->setContentsMargins(0, 0, 0, 0); speedLayout->setSpacing(2); QLabel *speedLabel = new QLabel("走钞速度"); speedLabel->setStyleSheet("color:#388087; font-weight: bold;"); speedLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); speedLayout->addWidget(speedLabel); speedButton = new QPushButton("设置机器走钞速度"); speedButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); speedButton->setStyleSheet(buttonStyle); speedLayout->addWidget(speedButton); grid->addWidget(speedWidget, 4, 0); //版本清分按钮 QWidget *versionSortWidget = new QWidget(); QVBoxLayout *versionSortLayout = new QVBoxLayout(versionSortWidget); versionSortLayout->setContentsMargins(0, 0, 0, 0); versionSortLayout->setSpacing(2); QLabel *versionSortLabel = new QLabel("版本清分"); versionSortLabel->setStyleSheet("color:#388087; font-weight: bold;"); versionSortLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); versionSortLayout->addWidget(versionSortLabel); versionSortButton = new QPushButton("设置版本清分"); versionSortButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); versionSortButton->setStyleSheet(buttonStyle); versionSortLayout->addWidget(versionSortButton); grid->addWidget(versionSortWidget, 4, 1); //退钞口张数按钮 QWidget *rejectCountWidget = new QWidget(); QVBoxLayout *rejectCountLayout = new QVBoxLayout(rejectCountWidget); rejectCountLayout->setContentsMargins(0, 0, 0, 0); rejectCountLayout->setSpacing(2); QLabel *rejectCountLabel = new QLabel("退钞口张数"); rejectCountLabel->setStyleSheet("color:#388087; font-weight: bold;"); rejectCountLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); rejectCountLayout->addWidget(rejectCountLabel); rejectCountButton = new QPushButton("设置退钞口张数"); rejectCountButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); rejectCountButton->setStyleSheet(buttonStyle); rejectCountLayout->addWidget(rejectCountButton); grid->addWidget(rejectCountWidget, 5, 0); //查询货币组合按钮 QWidget *queryCurrencyWidget = new QWidget(); QVBoxLayout *queryCurrencyLayout = new QVBoxLayout(queryCurrencyWidget); queryCurrencyLayout->setContentsMargins(0, 0, 0, 0); queryCurrencyLayout->setSpacing(2); QLabel *queryCurrencyLabel = new QLabel("货币组合"); queryCurrencyLabel->setStyleSheet("color:#388087; font-weight: bold;"); queryCurrencyLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); queryCurrencyLayout->addWidget(queryCurrencyLabel); queryCurrencyButton = new QPushButton("查询货币组合"); queryCurrencyButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); queryCurrencyButton->setStyleSheet(buttonStyle); queryCurrencyLayout->addWidget(queryCurrencyButton); grid->addWidget(queryCurrencyWidget, 5, 1); QWidget *detailedListWidget = new QWidget(); QVBoxLayout *detailedListLayout = new QVBoxLayout(detailedListWidget); detailedListLayout->setContentsMargins(0, 0, 0, 0); detailedListLayout->setSpacing(2); QLabel *emptyLabel8 = new QLabel("明细列表"); emptyLabel8->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel8->setAlignment(Qt::AlignLeft | Qt::AlignBottom); detailedListLayout->addWidget(emptyLabel8); detailedlistButton = new QPushButton("获取明细列表"); detailedlistButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); detailedlistButton->setStyleSheet(buttonStyle); detailedListLayout->addWidget(detailedlistButton); grid->addWidget(detailedListWidget, 6, 0); QWidget *rejectListWidget = new QWidget(); QVBoxLayout *rejectListLayout = new QVBoxLayout(rejectListWidget); rejectListLayout->setContentsMargins(0, 0, 0, 0); rejectListLayout->setSpacing(2); QLabel *emptyLabel7 = new QLabel("退钞口信息列表"); emptyLabel7->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel7->setAlignment(Qt::AlignLeft | Qt::AlignBottom); rejectListLayout->addWidget(emptyLabel7); rejectListButton = new QPushButton("获取退钞口信息列表"); rejectListButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); rejectListButton->setStyleSheet(buttonStyle); rejectListLayout->addWidget(rejectListButton); grid->addWidget(rejectListWidget, 6, 1); //获取传感器电压 QWidget *SensorvoltageWidget = new QWidget(); QVBoxLayout *SensorvoltageLayout = new QVBoxLayout(SensorvoltageWidget); SensorvoltageLayout->setContentsMargins(0, 0, 0, 0); SensorvoltageLayout->setSpacing(2); QLabel *SensorvoltageLabel = new QLabel("传感器电压"); SensorvoltageLabel->setStyleSheet("color:#388087; font-weight: bold;"); SensorvoltageLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); SensorvoltageLayout->addWidget(SensorvoltageLabel); SensorvoltageButton = new QPushButton("获取传感器电压"); SensorvoltageButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); SensorvoltageButton->setStyleSheet(buttonStyle); SensorvoltageLayout->addWidget(SensorvoltageButton); grid->addWidget(SensorvoltageWidget, 7, 0); //防钓鱼传感器按钮 QWidget *antiFishWidget = new QWidget(); QVBoxLayout *antiFishLayout = new QVBoxLayout(antiFishWidget); antiFishLayout->setContentsMargins(0, 0, 0, 0); antiFishLayout->setSpacing(2); QLabel *antiFishLabel = new QLabel("防钓鱼"); antiFishLabel->setStyleSheet("color:#388087; font-weight: bold;"); antiFishLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); antiFishLayout->addWidget(antiFishLabel); ansiFishButton = new QPushButton("防钓鱼传感器"); ansiFishButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); ansiFishButton->setStyleSheet(buttonStyle); antiFishLayout->addWidget(ansiFishButton); grid->addWidget(antiFishWidget, 7, 1); //连接信号槽 connect(testButton, &QPushButton::clicked, this, &MainWindow::onGetDetailListClicked); connect(handshakeButton, &QPushButton::clicked, this, &MainWindow::onHandshakeClicked); connect(workModeButton, &QPushButton::clicked, this, &MainWindow::onWorkModeClicked); connect(currencyButton, &QPushButton::clicked, this, &MainWindow::onCurrencyButtonClicked); connect(presetButton, &QPushButton::clicked, this, &MainWindow::onPresetButtonClicked); connect(presetAmountButton, &QPushButton::clicked, this, &MainWindow::onPresetAmountButtonClicked); connect(rejectListButton, &QPushButton::clicked, this, &MainWindow::onGetRejectListClicked); connect(detailedlistButton, &QPushButton::clicked,this,&MainWindow::onDetailedlistClicked); connect(SensorvoltageButton, &QPushButton::clicked, this, &MainWindow::onSensorvoltageClicked); connect(speedButton,&QPushButton::clicked,this,&MainWindow::onSpeedButtonClicked); connect(versionSortButton,&QPushButton::clicked,this,&MainWindow::onVersionSortButtonClicked); connect(rejectCountButton, &QPushButton::clicked, this, &MainWindow::onRejectCountButtonClicked); connect(queryCurrencyButton, &QPushButton::clicked, this, &MainWindow::onSelectCurrency); connect(ansiFishButton,&QPushButton::clicked,this,&MainWindow::onAntiFishClicked); connect(startMachineButton, &QPushButton::clicked, this, &MainWindow::onStartMachineClicked); connect(handleSelfButton, &QPushButton::clicked, this, &MainWindow::onhandleSelfClicked); return page; } QWidget* MainWindow::createSecondCommandPage() { QWidget *page = new QWidget(); QGridLayout *grid = new QGridLayout(page); grid->setSpacing(10); grid->setContentsMargins(10, 10, 10, 10); //蜂鸣器按钮 QWidget *buzzerWidget = new QWidget(); QVBoxLayout *buzzerLayout = new QVBoxLayout(buzzerWidget); buzzerLayout->setContentsMargins(0, 0, 0, 0); buzzerLayout->setSpacing(2); buzzerLabel = new QLabel("蜂鸣器"); buzzerLabel->setStyleSheet("color:#388087; font-weight: bold;"); buzzerLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); buzzerLayout->addWidget(buzzerLabel); buzzerButton = new QPushButton("设置机器蜂鸣器"); buzzerButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); buzzerButton->setStyleSheet(buttonStyle); buzzerLayout->addWidget(buzzerButton); grid->addWidget(buzzerWidget, 0, 0); //防尘罩按钮 - 无label QWidget *dustCoverWidget = new QWidget(); QVBoxLayout *dustCoverLayout = new QVBoxLayout(dustCoverWidget); dustCoverLayout->setContentsMargins(0, 0, 0, 0); dustCoverLayout->setSpacing(2); //添加一个空的占位label,保持高度一致 QLabel *emptyLabel1 = new QLabel(); emptyLabel1->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel1->setAlignment(Qt::AlignLeft | Qt::AlignBottom); dustCoverLayout->addWidget(emptyLabel1); QPushButton *openDustCoverButton = new QPushButton("打开防尘罩"); openDustCoverButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); openDustCoverButton->setStyleSheet(buttonStyle); dustCoverLayout->addWidget(openDustCoverButton); grid->addWidget(dustCoverWidget,0, 1); //底部挡板按钮 - 无label QWidget *bottomGateWidget = new QWidget(); QVBoxLayout *bottomGateLayout = new QVBoxLayout(bottomGateWidget); bottomGateLayout->setContentsMargins(0, 0, 0, 0); bottomGateLayout->setSpacing(2); QLabel *emptyLabel2 = new QLabel(); emptyLabel2->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel2->setAlignment(Qt::AlignLeft | Qt::AlignBottom); bottomGateLayout->addWidget(emptyLabel2); QPushButton *openBottomGateButton = new QPushButton("打开底部挡板"); openBottomGateButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); openBottomGateButton->setStyleSheet(buttonStyle); bottomGateLayout->addWidget(openBottomGateButton); grid->addWidget(bottomGateWidget, 1, 0); //关闭防尘罩按钮 - 无label QWidget *closeDustCoverWidget = new QWidget(); QVBoxLayout *closeDustCoverLayout = new QVBoxLayout(closeDustCoverWidget); closeDustCoverLayout->setContentsMargins(0, 0, 0, 0); closeDustCoverLayout->setSpacing(2); QLabel *emptyLabel5 = new QLabel(); emptyLabel5->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel5->setAlignment(Qt::AlignLeft | Qt::AlignBottom); closeDustCoverLayout->addWidget(emptyLabel5); QPushButton *closeDustCoverButton = new QPushButton("关闭防尘罩"); closeDustCoverButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); closeDustCoverButton->setStyleSheet(buttonStyle); closeDustCoverLayout->addWidget(closeDustCoverButton); grid->addWidget(closeDustCoverWidget, 1, 1); //关闭底部挡板按钮 - 无label QWidget *closeBottomGateWidget = new QWidget(); QVBoxLayout *closeBottomGateLayout = new QVBoxLayout(closeBottomGateWidget); closeBottomGateLayout->setContentsMargins(0, 0, 0, 0); closeBottomGateLayout->setSpacing(2); QLabel *emptyLabel6 = new QLabel(); emptyLabel6->setStyleSheet("color:#388087; font-weight: bold;"); emptyLabel6->setAlignment(Qt::AlignLeft | Qt::AlignBottom); closeBottomGateLayout->addWidget(emptyLabel6); QPushButton *closeBottomGateButton = new QPushButton("关闭底部挡板"); closeBottomGateButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); closeBottomGateButton->setStyleSheet(buttonStyle); closeBottomGateLayout->addWidget(closeBottomGateButton); grid->addWidget(closeBottomGateWidget, 2, 0); //CIS校验按钮 QWidget *cisCheckWidget = new QWidget(); QVBoxLayout *cisCheckLayout = new QVBoxLayout(cisCheckWidget); cisCheckLayout->setContentsMargins(0, 0, 0, 0); cisCheckLayout->setSpacing(2); QLabel *cisCheckLabel = new QLabel("CIS校验"); cisCheckLabel->setStyleSheet("color:#388087; font-weight: bold;"); cisCheckLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); cisCheckLayout->addWidget(cisCheckLabel); cisCheckButton = new QPushButton("CIS传感器校验"); cisCheckButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); cisCheckButton->setStyleSheet(buttonStyle); cisCheckLayout->addWidget(cisCheckButton); grid->addWidget(cisCheckWidget, 2, 1); //出厂设置 QWidget *factoryResetWidget = new QWidget(); QVBoxLayout *factoryResetLayout = new QVBoxLayout(factoryResetWidget); factoryResetLayout->setContentsMargins(0, 0, 0, 0); factoryResetLayout->setSpacing(2); QLabel *factoryResetLabel = new QLabel(); factoryResetLabel->setStyleSheet("color:#388087; font-weight: bold;"); factoryResetLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); factoryResetLayout->addWidget(factoryResetLabel); QPushButton *factoryResetButton = new QPushButton("恢复出厂设置"); factoryResetButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); factoryResetButton->setStyleSheet(buttonStyle); factoryResetLayout->addWidget(factoryResetButton); grid->addWidget(factoryResetWidget, 3, 0); //按键功能按钮 QWidget *pressWidget = new QWidget(); QVBoxLayout *pressLayout = new QVBoxLayout(pressWidget); pressLayout->setContentsMargins(0, 0, 0, 0); pressLayout->setSpacing(2); QLabel *pressLabel = new QLabel("按键功能"); pressLabel->setStyleSheet("color:#388087; font-weight: bold;"); pressLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); pressLayout->addWidget(pressLabel); pressButton = new QPushButton("按键功能"); pressButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); pressButton->setStyleSheet(buttonStyle); pressLayout->addWidget(pressButton); grid->addWidget(pressWidget, 3, 1); //老化命令 QWidget *agingWidget = new QWidget(); QVBoxLayout *agingLayout = new QVBoxLayout(agingWidget); agingLayout->setContentsMargins(0, 0, 0, 0); agingLayout->setSpacing(2); QLabel *agingLabel = new QLabel("老化"); agingLabel->setStyleSheet("color:#388087; font-weight: bold;"); agingLabel->setAlignment(Qt::AlignLeft | Qt::AlignBottom); agingLayout->addWidget(agingLabel); agingButton = new QPushButton("准备老化"); agingButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); agingButton->setStyleSheet(buttonStyle); agingLayout->addWidget(agingButton); grid->addWidget(agingWidget, 4, 0); grid->setRowStretch(5, 1); //让第5行(索引4)占据剩余空间 //连接信号槽 connect(cisCheckButton, &QPushButton::clicked, this, [this](){ if(!m_serialWorker) { logMessage("串口未打开"); return; } setButtonActive(qobject_cast<QPushButton*>(sender())); sendCISCheckCommand(); }); connect(pressButton,&QPushButton::clicked,this,&MainWindow::onPressButtonClicked); connect(openDustCoverButton, &QPushButton::clicked, this, &MainWindow::onOpenDustCoverClicked); connect(openBottomGateButton, &QPushButton::clicked, this, &MainWindow::onOpenBottomGateClicked); connect(closeDustCoverButton, &QPushButton::clicked, this, &MainWindow::onCloseDustCoverClicked); connect(closeBottomGateButton, &QPushButton::clicked, this, &MainWindow::onCloseBottomGateClicked); connect(buzzerButton, &QPushButton::clicked, this, &MainWindow::onSetBuzzerClicked); connect(factoryResetButton, &QPushButton::clicked, this, &MainWindow::onFactoryResetClicked); connect(agingButton, &QPushButton::clicked, this, &MainWindow::onAgingClicked); return page; } #include "serialworker.h" #include <QDebug> #include <QSerialPortInfo> SerialWorker::SerialWorker(const QString &portName, qint32 baudRate, QObject *parent) : QThread(parent), m_portName(portName), m_serialPort(nullptr), m_running(false), m_baudRate(baudRate) { } SerialWorker::~SerialWorker() { stop(); } //设置波特率 void SerialWorker::setBaudRate(qint32 baudRate) { m_baudRate = baudRate; if(m_serialPort && m_serialPort->isOpen()) { m_serialPort->setBaudRate(m_baudRate); } } void SerialWorker::run() { // 检查串口是否已存在 bool portAvailable = false; const auto ports = QSerialPortInfo::availablePorts(); for(const QSerialPortInfo &port : ports) { if(port.portName() == m_portName) { portAvailable = true; break; } } if(!portAvailable) { emit errorOccurred(tr("串口 %1 不存在").arg(m_portName)); return; } m_serialPort = new QSerialPort(); m_serialPort->setPortName(m_portName); m_serialPort->setBaudRate(m_baudRate); //成员变量设置波特率 m_serialPort->setDataBits(QSerialPort::Data8); m_serialPort->setParity(QSerialPort::NoParity); m_serialPort->setStopBits(QSerialPort::OneStop); m_serialPort->setFlowControl(QSerialPort::NoFlowControl); if(!m_serialPort->open(QIODevice::ReadWrite)) { emit errorOccurred(tr("无法打开串口 %1: %2").arg(m_portName).arg(m_serialPort->errorString())); delete m_serialPort; m_serialPort = nullptr; return; } m_running = true; //读取串口数据 while(m_running) { if(m_serialPort->waitForReadyRead(100)) { //等待数据到达 QByteArray data = m_serialPort->readAll(); //读取可用数据 //尝试读取剩余数据 while(m_serialPort->waitForReadyRead(10)) { data += m_serialPort->readAll(); } emit dataReceived(data); //发送接收到的数据 } //检查串口是否仍然打开 if(!m_serialPort->isOpen()) { emit errorOccurred(tr("串口 %1 意外关闭").arg(m_portName)); break; } } //清理串口对象 if(m_serialPort) { m_serialPort->close(); delete m_serialPort; m_serialPort = nullptr; } } void SerialWorker::stop() { m_running = false; wait(); } void SerialWorker::send(const QByteArray &data) { //发送数据到串口 if(m_serialPort && m_serialPort->isOpen()) { if(m_serialPort->write(data) == -1) { emit errorOccurred(tr("发送数据失败: %1").arg(m_serialPort->errorString())); } } } 这个能否增加一个识别数据线是否断开呢,如果数据线断开我的PC就主动断开串口,并且在断开串口前在logMessage显示数据线已断开

import sys import cv2 import time import torch import traceback import threading import queue import dxcam import ctypes import os import glob import numpy as np import logitech.lg from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QSlider, QSpinBox, QDoubleSpinBox, QLineEdit, QTabWidget, QGroupBox, QTextEdit, QFileDialog, QMessageBox, QSizePolicy, QSplitter, QDialog, QScrollArea) from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal from PyQt6.QtGui import QImage, QPixmap, QPainter, QColor, QFont, QIcon, QKeyEvent, QMouseEvent from PyQt6.QtSvg import QSvgRenderer from PIL import Image from ultralytics import YOLO from pynput import mouse class PIDController: """PID控制器""" def __init__(self, kp, ki, kd, output_min=-100, output_max=100): self.kp = kp # 比例增益 self.ki = ki # 积分增益 self.kd = kd # 微分增益 self.output_min = output_min self.output_max = output_max # 状态变量 self.integral = 0.0 self.prev_error = 0.0 self.last_time = time.perf_counter() def compute(self, setpoint, current_value): """计算PID控制输出""" current_time = time.perf_counter() dt = current_time - self.last_time # 防止过小的时间差导致计算问题 MIN_DT = 0.0001 if dt < MIN_DT: dt = MIN_DT # 计算误差 error = setpoint - current_value # 比例项 P = self.kp * error # 积分项(防饱和) self.integral += error * dt I = self.ki * self.integral # 微分项 derivative = (error - self.prev_error) / dt D = self.kd * derivative # 合成输出 output = P + I + D # 输出限幅 if output > self.output_max: output = self.output_max elif output < self.output_min: output = self.output_min # 更新状态 self.prev_error = error self.last_time = current_time return output def reset(self): """重置控制器状态""" self.integral = 0.0 self.prev_error = 0.0 self.last_time = time.perf_counter() class ScreenDetector: def __init__(self, config_path): # 解析配置文件 self._parse_config(config_path) # 设备检测与模型加载 self.device = self._determine_device() self.model = YOLO(self.model_path).to(self.device) # 屏幕信息初始化 self._init_screen_info() # 控制参数初始化 self._init_control_params() # 状态管理 self.stop_event = threading.Event() self.camera_lock = threading.Lock() self.target_lock = threading.Lock() self.offset_lock = threading.Lock() self.button_lock = threading.Lock() # 推理状态控制 self.inference_active = False self.inference_lock = threading.Lock() # 初始化相机 self._init_camera() # 初始化鼠标监听器 self._init_mouse_listener() # 初始化PID控制器 self._init_pid_controllers() def _parse_config(self, config_path): """解析并存储配置参数""" self.cfg = self._parse_txt_config(config_path) # 存储常用参数 self.model_path = self.cfg['model_path'] self.model_device = self.cfg['model_device'] self.screen_target_size = int(self.cfg['screen_target_size']) self.detection_conf_thres = float(self.cfg['detection_conf_thres']) self.detection_iou_thres = float(self.cfg['detection_iou_thres']) self.detection_classes = [int(x) for x in self.cfg['detection_classes'].split(',')] self.visualization_color = tuple(map(int, self.cfg['visualization_color'].split(','))) self.visualization_line_width = int(self.cfg['visualization_line_width']) self.visualization_font_scale = float(self.cfg['visualization_font_scale']) self.visualization_show_conf = bool(self.cfg['visualization_show_conf']) self.fov_horizontal = float(self.cfg.get('move_fov_horizontal', '90')) self.mouse_dpi = int(self.cfg.get('move_mouse_dpi', '400')) self.target_offset_x_percent = float(self.cfg.get('target_offset_x', '50')) self.target_offset_y_percent = 100 - float(self.cfg.get('target_offset_y', '50')) # PID参数 self.pid_kp = float(self.cfg.get('pid_kp', '1.0')) self.pid_ki = float(self.cfg.get('pid_ki', '0.05')) self.pid_kd = float(self.cfg.get('pid_kd', '0.2')) # 贝塞尔曲线参数 self.bezier_steps = int(self.cfg.get('bezier_steps', '100')) self.bezier_duration = float(self.cfg.get('bezier_duration', '0.1')) self.bezier_curve = float(self.cfg.get('bezier_curve', '0.3')) def update_config(self, config_path): """动态更新配置""" try: # 重新解析配置文件 self._parse_config(config_path) # 更新可以直接修改的参数 self.detection_conf_thres = float(self.cfg['detection_conf_thres']) self.detection_iou_thres = float(self.cfg['detection_iou_thres']) self.target_offset_x_percent = float(self.cfg.get('target_offset_x', '50')) self.target_offset_y_percent = 100 - float(self.cfg.get('target_offset_y', '50')) # PID参数更新 self.pid_kp = float(self.cfg.get('pid_kp', '1.0')) self.pid_ki = float(self.cfg.get('pid_ki', '0.05')) self.pid_kd = float(self.cfg.get('pid_kd', '0.2')) # 更新PID控制器 self.pid_x = PIDController(self.pid_kp, self.pid_ki, self.pid_kd) self.pid_y = PIDController(self.pid_kp, self.pid_ki, self.pid_kd) # FOV和DPI更新 self.fov_horizontal = float(self.cfg.get('move_fov_horizontal', '90')) self.mouse_dpi = int(self.cfg.get('move_mouse_dpi', '400')) # 更新贝塞尔曲线参数 self.bezier_steps = int(self.cfg.get('bezier_steps', '100')) self.bezier_duration = float(self.cfg.get('bezier_duration', '0.1')) self.bezier_curve = float(self.cfg.get('bezier_curve', '0.3')) print("配置已动态更新") return True except Exception as e: print(f"更新配置失败: {str(e)}") traceback.print_exc() return False def _parse_txt_config(self, path): """解析TXT格式的配置文件""" config = {} with open(path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue if '=' in line: key, value = line.split('=', 1) config[key.strip()] = value.strip() return config def _init_pid_controllers(self): """初始化PID控制器""" # 创建XY方向的PID控制器 self.pid_x = PIDController(self.pid_kp, self.pid_ki, self.pid_kd) self.pid_y = PIDController(self.pid_kp, self.pid_ki, self.pid_kd) def start_inference(self): """启动推理""" with self.inference_lock: self.inference_active = True def stop_inference(self): """停止推理""" with self.inference_lock: self.inference_active = False def _determine_device(self): """确定运行设备""" if self.model_device == 'auto': return 'cuda' if torch.cuda.is_available() and torch.cuda.device_count() > 0 else 'cpu' return self.model_device def _init_screen_info(self): """初始化屏幕信息""" user32 = ctypes.windll.user32 self.screen_width, self.screen_height = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1) self.screen_center = (self.screen_width // 2, self.screen_height // 2) # 计算截图区域 left = (self.screen_width - self.screen_target_size) // 2 top = (self.screen_height - self.screen_target_size) // 2 self.region = ( max(0, int(left)), max(0, int(top)), min(self.screen_width, int(left + self.screen_target_size)), min(self.screen_height, int(top + self.screen_target_size)) ) def _init_control_params(self): """初始化控制参数""" self.previous_target_info = None self.closest_target_absolute = None self.target_offset = None self.right_button_pressed = False # 改为鼠标右键状态 def _init_camera(self): """初始化相机""" try: with self.camera_lock: self.camera = dxcam.create( output_idx=0, output_color="BGR", region=self.region ) self.camera.start(target_fps=120, video_mode=True) except Exception as e: print(f"相机初始化失败: {str(e)}") try: # 降级模式 with self.camera_lock: self.camera = dxcam.create() self.camera.start(target_fps=60, video_mode=True) except Exception as fallback_e: print(f"降级模式初始化失败: {str(fallback_e)}") self.camera = None def _init_mouse_listener(self): """初始化鼠标监听器""" self.mouse_listener = mouse.Listener( on_click=self.on_mouse_click # 监听鼠标点击事件 ) self.mouse_listener.daemon = True self.mouse_listener.start() def on_mouse_click(self, x, y, button, pressed): """处理鼠标点击事件""" try: if button == mouse.Button.right: # 监听鼠标右键 with self.button_lock: self.right_button_pressed = pressed # 更新状态 # 当右键释放时重置PID控制器 if not pressed: self.pid_x.reset() self.pid_y.reset() except Exception as e: print(f"鼠标事件处理错误: {str(e)}") def calculate_fov_movement(self, dx, dy): """基于FOV算法计算鼠标移动量""" # 计算屏幕对角线长度 screen_diagonal = (self.screen_width ** 2 + self.screen_height ** 2) ** 0.5 # 计算垂直FOV aspect_ratio = self.screen_width / self.screen_height fov_vertical = self.fov_horizontal / aspect_ratio # 计算每像素对应角度 angle_per_pixel_x = self.fov_horizontal / self.screen_width angle_per_pixel_y = fov_vertical / self.screen_height # 计算角度偏移 angle_offset_x = dx * angle_per_pixel_x angle_offset_y = dy * angle_per_pixel_y # 转换为鼠标移动量 move_x = (angle_offset_x / 360) * self.mouse_dpi move_y = (angle_offset_y / 360) * self.mouse_dpi return move_x, move_y def move_mouse_to_target(self): """移动鼠标对准目标点""" if not self.target_offset: return try: # 获取目标点与屏幕中心的偏移量 with self.offset_lock: dx, dy = self.target_offset # 使用FOV算法将像素偏移转换为鼠标移动量 move_x, move_y = self.calculate_fov_movement(dx, dy) # 使用PID计算平滑的移动量 pid_move_x = self.pid_x.compute(0, -move_x) # 将dx取反 pid_move_y = self.pid_y.compute(0, -move_y) # 将dy取反 # 移动鼠标 if pid_move_x != 0 or pid_move_y != 0: logitech.lg.start_mouse_move(int(pid_move_x), int(pid_move_y), self.bezier_steps, self.bezier_duration, self.bezier_curve) except Exception as e: print(f"移动鼠标时出错: {str(e)}") def run(self, frame_queue): """主检测循环""" while not self.stop_event.is_set(): try: # 检查推理状态 with self.inference_lock: if not self.inference_active: time.sleep(0.01) continue # 截图 grab_start = time.perf_counter() screenshot = self._grab_screenshot() grab_time = (time.perf_counter() - grab_start) * 1000 # ms if screenshot is None: time.sleep(0.001) continue # 推理 inference_start = time.perf_counter() results = self._inference(screenshot) inference_time = (time.perf_counter() - inference_start) * 1000 # ms # 处理检测结果 target_info, closest_target_relative, closest_offset = self._process_detection_results(results) # 更新目标信息 self._update_target_info(target_info, closest_offset) # 移动鼠标 self._move_mouse_if_needed() # 可视化处理 annotated_frame = self._visualize_results(results, closest_target_relative) if frame_queue else None # 放入队列 if frame_queue: try: frame_queue.put( (annotated_frame, len(target_info), inference_time, grab_time, target_info), timeout=0.01 ) except queue.Full: pass except Exception as e: print(f"检测循环异常: {str(e)}") traceback.print_exc() self._reset_camera() time.sleep(0.5) def _grab_screenshot(self): """安全获取截图""" with self.camera_lock: if self.camera: return self.camera.grab() return None def _inference(self, screenshot): """执行模型推理""" return self.model.predict( screenshot, conf=self.detection_conf_thres, iou=self.detection_iou_thres, classes=self.detection_classes, device=self.device, verbose=False ) def _process_detection_results(self, results): """处理检测结果""" target_info = [] min_distance = float('inf') closest_target_relative = None closest_target_absolute = None closest_offset = None for box in results[0].boxes: # 获取边界框坐标 x1, y1, x2, y2 = map(int, box.xyxy[0]) # 计算绝对坐标 x1_abs = x1 + self.region[0] y1_abs = y1 + self.region[1] x2_abs = x2 + self.region[0] y2_abs = y2 + self.region[1] # 计算边界框尺寸 width = x2_abs - x1_abs height = y2_abs - y1_abs # 应用偏移百分比计算目标点 target_x = x1_abs + int(width * (self.target_offset_x_percent / 100)) target_y = y1_abs + int(height * (self.target_offset_y_percent / 100)) # 计算偏移量 dx = target_x - self.screen_center[0] dy = target_y - self.screen_center[1] distance = (dx ** 2 + dy ** 2) ** 0.5 # 更新最近目标 if distance < min_distance: min_distance = distance # 计算相对坐标(用于可视化) closest_target_relative = ( x1 + int(width * (self.target_offset_x_percent / 100)), y1 + int(height * (self.target_offset_y_percent / 100)) ) closest_target_absolute = (target_x, target_y) closest_offset = (dx, dy) # 保存目标信息 class_id = int(box.cls) class_name = self.model.names[class_id] target_info.append(f"{class_name}:{x1_abs},{y1_abs},{x2_abs},{y2_abs}") return target_info, closest_target_relative, closest_offset def _update_target_info(self, target_info, closest_offset): """更新目标信息""" # 检查目标信息是否有变化 if target_info != self.previous_target_info: self.previous_target_info = target_info.copy() print(f"{len(target_info)}|{'|'.join(target_info)}") # 更新目标偏移量 with self.offset_lock: self.target_offset = closest_offset def _visualize_results(self, results, closest_target): """可视化处理结果""" frame = results[0].plot( line_width=self.visualization_line_width, font_size=self.visualization_font_scale, conf=self.visualization_show_conf ) # 绘制最近目标 if closest_target: # 绘制目标中心点 cv2.circle( frame, (int(closest_target[0]), int(closest_target[1])), 3, (0, 0, 255), -1 ) # 计算屏幕中心在截图区域内的相对坐标 screen_center_x = self.screen_center[0] - self.region[0] screen_center_y = self.screen_center[1] - self.region[1] # 绘制中心到目标的连线 cv2.line( frame, (int(screen_center_x), int(screen_center_y)), (int(closest_target[0]), int(closest_target[1])), (0, 255, 0), 1 ) return frame def _move_mouse_if_needed(self): """如果需要则移动鼠标""" with self.button_lock: if self.right_button_pressed and self.target_offset: # 使用right_button_pressed self.move_mouse_to_target() def _reset_camera(self): """重置相机""" print("正在重置相机...") try: self._init_camera() except Exception as e: print(f"相机重置失败: {str(e)}") traceback.print_exc() def stop(self): """安全停止检测器""" self.stop_event.set() self._safe_stop() if hasattr(self, 'mouse_listener') and self.mouse_listener.running: # 改为停止鼠标监听器 self.mouse_listener.stop() def _safe_stop(self): """同步释放资源""" print("正在安全停止相机...") try: with self.camera_lock: if self.camera: self.camera.stop() print("相机已停止") except Exception as e: print(f"停止相机时发生错误: {str(e)}") print("屏幕检测器已停止") class DetectionThread(QThread): update_signal = pyqtSignal(object) def __init__(self, detector, frame_queue): super().__init__() self.detector = detector self.frame_queue = frame_queue self.running = True def run(self): self.detector.run(self.frame_queue) def stop(self): self.running = False self.detector.stop() class MainWindow(QMainWindow): def __init__(self, detector): super().__init__() self.detector = detector self.setWindowTitle("EFAI 1.1") self.setGeometry(100, 100, 600, 400) # 添加缺失的属性初始化 self.visualization_enabled = True self.inference_active = False # 初始推理状态为停止 #窗口置顶 self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint) # 创建帧队列 self.frame_queue = queue.Queue(maxsize=3) # 初始化UI self.init_ui() # 启动检测线程 self.detection_thread = DetectionThread(self.detector, self.frame_queue) self.detection_thread.start() # 启动UI更新定时器 self.update_timer = QTimer() self.update_timer.timeout.connect(self.update_ui) self.update_timer.start(1) # 每1ms更新一次 def toggle_visualization(self): # 实际更新可视化状态属性 self.visualization_enabled = not self.visualization_enabled # 更新按钮文本 if self.visualization_enabled: self.toggle_visualization_btn.setText("禁用可视化") else: self.toggle_visualization_btn.setText("启用可视化") def toggle_inference(self): """切换推理状态""" self.inference_active = not self.inference_active if self.inference_active: self.toggle_inference_btn.setText("停止推理") self.toggle_inference_btn.setStyleSheet(""" QPushButton { background-color: #F44336; color: white; border: none; padding: 8px; border-radius: 4px; font-family: Segoe UI; font-size: 10pt; } """) self.detector.start_inference() else: self.toggle_inference_btn.setText("开始推理") self.toggle_inference_btn.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-family: Segoe UI; font-size: 10pt; } """) self.detector.stop_inference() def init_ui(self): # 主布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 分割器(左侧图像/目标信息,右侧控制面板) splitter = QSplitter(Qt.Orientation.Horizontal) main_layout.addWidget(splitter) # 左侧区域(图像显示和目标信息) left_widget = QWidget() left_layout = QVBoxLayout(left_widget) # 图像显示区域 self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.image_label.setMinimumSize(320, 320) left_layout.addWidget(self.image_label) # 目标信息区域 self.target_info_text = QTextEdit() self.target_info_text.setReadOnly(True) self.target_info_text.setFixedHeight(150) self.target_info_text.setStyleSheet(""" QTextEdit { background-color: #2D2D30; color: #DCDCDC; font-family: Consolas; font-size: 10pt; border: 1px solid #3F3F46; border-radius: 4px; } """) left_layout.addWidget(self.target_info_text) # 右侧控制面板 right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # 性能信息 perf_group = QGroupBox("性能信息") perf_layout = QVBoxLayout(perf_group) self.target_count_label = QLabel("目标数量: 0") self.inference_time_label = QLabel("推理时间: 0.000s") self.grab_time_label = QLabel("截图时间: 0.000s") for label in [self.target_count_label, self.inference_time_label, self.grab_time_label]: label.setStyleSheet("font-family: Consolas; font-size: 10pt;") perf_layout.addWidget(label) right_layout.addWidget(perf_group) # 系统信息 sys_group = QGroupBox("系统信息") sys_layout = QVBoxLayout(sys_group) # 获取模型名称(只显示文件名) model_name = os.path.basename(self.detector.model_path) # 获取显示器编号(如果配置中有则显示,否则显示默认值0) monitor_index = self.detector.cfg.get('screen_monitor', '0') self.model_label = QLabel(f"模型: {model_name}") self.device_label = QLabel(f"设备: {self.detector.device.upper()}") self.monitor_label = QLabel(f"显示器:{monitor_index}") self.screen_res_label = QLabel(f"屏幕分辨率: {self.detector.screen_width}x{self.detector.screen_height}") self.region_label = QLabel(f"检测区域: {self.detector.region}") for label in [self.model_label, self.device_label, self.monitor_label, self.screen_res_label, self.region_label]: label.setStyleSheet("font-family: Consolas; font-size: 9pt; color: #A0A0A0;") sys_layout.addWidget(label) right_layout.addWidget(sys_group) # 鼠标状态 mouse_group = QGroupBox("自瞄状态") mouse_layout = QVBoxLayout(mouse_group) self.mouse_status = QLabel("未瞄准") self.mouse_status.setStyleSheet(""" QLabel { font-family: Consolas; font-size: 10pt; color: #FF5252; } """) mouse_layout.addWidget(self.mouse_status) right_layout.addWidget(mouse_group) # 控制按钮 btn_group = QGroupBox("控制") btn_layout = QVBoxLayout(btn_group) # 添加推理切换按钮 self.toggle_inference_btn = QPushButton("开始推理") self.toggle_inference_btn.clicked.connect(self.toggle_inference) self.toggle_inference_btn.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px; border-radius: 4px; font-family: Segoe UI; font-size: 10pt; } QPushButton:hover { background-color: #45A049; } QPushButton:pressed { background-color: #3D8B40; } """) btn_layout.addWidget(self.toggle_inference_btn) self.toggle_visualization_btn = QPushButton("禁用可视化") self.toggle_visualization_btn.clicked.connect(self.toggle_visualization) self.settings_btn = QPushButton("设置") self.settings_btn.clicked.connect(self.open_settings) for btn in [self.toggle_visualization_btn, self.settings_btn]: btn.setStyleSheet(""" QPushButton { background-color: #0078D7; color: white; border: none; padding: 8px; border-radius: 4px; font-family: Segoe UI; font-size: 10pt; } QPushButton:hover { background-color: #106EBE; } QPushButton:pressed { background-color: #005A9E; } """) btn_layout.addWidget(btn) right_layout.addWidget(btn_group) # 添加左右区域到分割器 splitter.addWidget(left_widget) splitter.addWidget(right_widget) splitter.setSizes([600, 200]) # 设置样式 self.setStyleSheet(""" QMainWindow { background-color: #252526; } QGroupBox { font-family: Segoe UI; font-size: 10pt; color: #CCCCCC; border: 1px solid #3F3F46; border-radius: 4px; margin-top: 1ex; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; background-color: transparent; } """) def open_settings(self): settings_dialog = SettingsDialog(self.detector.cfg, self) settings_dialog.exec() def update_ui(self): try: # 获取最新数据 latest_data = None while not self.frame_queue.empty(): latest_data = self.frame_queue.get_nowait() if latest_data: # 解包数据 frame, targets_count, inference_time, grab_time, target_info = latest_data # 更新性能信息 self.target_count_label.setText(f"目标数量: {targets_count}") self.inference_time_label.setText(f"推理时间: {inference_time / 1000:.3f}s") self.grab_time_label.setText(f"截图时间: {grab_time / 1000:.3f}s") # 更新目标信息 self.display_target_info(target_info) # 更新图像显示 if self.visualization_enabled and frame is not None: # 转换图像为Qt格式 height, width, channel = frame.shape bytes_per_line = 3 * width q_img = QImage(frame.data, width, height, bytes_per_line, QImage.Format.Format_BGR888) pixmap = QPixmap.fromImage(q_img) # 等比例缩放 scaled_pixmap = pixmap.scaled( self.image_label.width(), self.image_label.height(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation ) self.image_label.setPixmap(scaled_pixmap) else: # 显示黑色背景 pixmap = QPixmap(self.image_label.size()) pixmap.fill(QColor(0, 0, 0)) self.image_label.setPixmap(pixmap) # 更新鼠标状态 self.update_mouse_status() except Exception as e: print(f"更新UI时出错: {str(e)}") def display_target_info(self, target_info): """在文本框中显示目标信息""" if not target_info: self.target_info_text.setPlainText("无检测目标") return info_text = "目标类别与坐标:\n" for i, data in enumerate(target_info): try: parts = data.split(":", 1) if len(parts) == 2: class_name, coords_str = parts coords = list(map(int, coords_str.split(','))) if len(coords) == 4: display_text = f"{class_name}: [{coords[0]}, {coords[1]}, {coords[2]}, {coords[3]}]" else: display_text = f"坐标格式错误: {data}" else: display_text = f"数据格式错误: {data}" except: display_text = f"解析错误: {data}" info_text += f"{display_text}\n" self.target_info_text.setPlainText(info_text) def update_mouse_status(self): """更新鼠标右键状态显示""" with self.detector.button_lock: if self.detector.right_button_pressed: self.mouse_status.setText("瞄准中") self.mouse_status.setStyleSheet("color: #4CAF50; font-family: Consolas; font-size: 10pt;") else: self.mouse_status.setText("未瞄准") self.mouse_status.setStyleSheet("color: #FF5252; font-family: Consolas; font-size: 10pt;") def closeEvent(self, event): """安全关闭程序""" self.detection_thread.stop() self.detection_thread.wait() event.accept() class SettingsDialog(QDialog): def __init__(self, config, parent=None): super().__init__(parent) self.config = config # 保存原始配置的副本用于比较 self.original_config = config.copy() self.setWindowTitle("设置") self.setGeometry(100, 100, 600, 500) self.init_ui() def init_ui(self): layout = QVBoxLayout() self.setLayout(layout) # 标签页 tabs = QTabWidget() layout.addWidget(tabs) # 检测设置标签页 detection_tab = QWidget() detection_layout = QVBoxLayout(detection_tab) self.create_detection_settings(detection_layout) tabs.addTab(detection_tab, "检测") # 移动设置标签页 move_tab = QWidget() move_layout = QVBoxLayout(move_tab) self.create_move_settings(move_layout) tabs.addTab(move_tab, "FOV") # 目标点设置标签页 target_tab = QWidget() target_layout = QVBoxLayout(target_tab) self.create_target_settings(target_layout) tabs.addTab(target_tab, "目标点") # PID设置标签页 pid_tab = QWidget() pid_layout = QVBoxLayout(pid_tab) self.create_pid_settings(pid_layout) tabs.addTab(pid_tab, "PID") # 贝塞尔曲线设置标签页 bezier_tab = QWidget() bezier_layout = QVBoxLayout(bezier_tab) self.create_bezier_settings(bezier_layout) tabs.addTab(bezier_tab, "贝塞尔曲线") # 按钮区域 btn_layout = QHBoxLayout() layout.addLayout(btn_layout) save_btn = QPushButton("保存配置") save_btn.clicked.connect(self.save_config) cancel_btn = QPushButton("取消") cancel_btn.clicked.connect(self.reject) for btn in [save_btn, cancel_btn]: btn.setStyleSheet(""" QPushButton { background-color: #0078D7; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-family: Segoe UI; font-size: 10pt; } QPushButton:hover { background-color: #106EBE; } QPushButton:pressed { background-color: #005A9E; } """) btn_layout.addWidget(btn) btn_layout.addStretch() def create_detection_settings(self, layout): # 模型选择 model_group = QGroupBox("模型设置") model_layout = QVBoxLayout(model_group) # 获取基础路径 if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.dirname(os.path.abspath(__file__)) # 获取模型文件列表 models_dir = os.path.join(base_path, 'models') model_files = [] if os.path.exists(models_dir): model_files = glob.glob(os.path.join(models_dir, '*.pt')) # 处理模型显示名称 model_display_names = [os.path.basename(f) for f in model_files] if model_files else ["未找到模型文件"] self.model_name_to_path = {os.path.basename(f): f for f in model_files} # 当前配置的模型处理 current_model_path = self.config['model_path'] current_model_name = os.path.basename(current_model_path) # 确保当前模型在列表中 if current_model_name not in model_display_names: model_display_names.append(current_model_name) self.model_name_to_path[current_model_name] = current_model_path # 模型选择下拉框 model_layout.addWidget(QLabel("选择模型:")) self.model_combo = QComboBox() self.model_combo.addItems(model_display_names) self.model_combo.setCurrentText(current_model_name) model_layout.addWidget(self.model_combo) # 设备选择 model_layout.addWidget(QLabel("运行设备:")) self.device_combo = QComboBox() self.device_combo.addItems(['auto', 'cuda', 'cpu']) self.device_combo.setCurrentText(self.config['model_device']) model_layout.addWidget(self.device_combo) layout.addWidget(model_group) # 检测参数 param_group = QGroupBox("检测参数") param_layout = QVBoxLayout(param_group) # 置信度阈值 param_layout.addWidget(QLabel("置信度阈值:")) conf_layout = QHBoxLayout() self.conf_slider = QSlider(Qt.Orientation.Horizontal) self.conf_slider.setRange(10, 100) # 0.1到1.0,步长0.01 self.conf_slider.setValue(int(float(self.config['detection_conf_thres']) * 100)) conf_layout.addWidget(self.conf_slider) self.conf_value = QLabel(f"{float(self.config['detection_conf_thres']):.2f}") self.conf_value.setFixedWidth(50) conf_layout.addWidget(self.conf_value) param_layout.addLayout(conf_layout) # 连接滑块值变化事件 self.conf_slider.valueChanged.connect(lambda value: self.conf_value.setText(f"{value / 100:.2f}")) # IOU阈值 - 改为滑动条 param_layout.addWidget(QLabel("IOU阈值:")) iou_layout = QHBoxLayout() self.iou_slider = QSlider(Qt.Orientation.Horizontal) self.iou_slider.setRange(10, 100) # 0.1到1.0,步长0.01 self.iou_slider.setValue(int(float(self.config['detection_iou_thres']) * 100)) iou_layout.addWidget(self.iou_slider) self.iou_value = QLabel(f"{float(self.config['detection_iou_thres']):.2f}") self.iou_value.setFixedWidth(50) iou_layout.addWidget(self.iou_value) param_layout.addLayout(iou_layout) # 连接滑块值变化事件 self.iou_slider.valueChanged.connect(lambda value: self.iou_value.setText(f"{value / 100:.2f}")) # 检测类别 param_layout.addWidget(QLabel("检测类别 (逗号分隔):")) self.classes_edit = QLineEdit() self.classes_edit.setText(self.config['detection_classes']) param_layout.addWidget(self.classes_edit) layout.addWidget(param_group) # 屏幕设置 screen_group = QGroupBox("屏幕设置") screen_layout = QVBoxLayout(screen_group) # 显示器编号 screen_layout.addWidget(QLabel("显示器编号:")) self.monitor_spin = QSpinBox() self.monitor_spin.setRange(0, 3) # 假设最多支持4个显示器 self.monitor_spin.setValue(int(self.config.get('screen_monitor', '0'))) screen_layout.addWidget(self.monitor_spin) # 屏幕区域大小 screen_layout.addWidget(QLabel("截屏尺寸:")) self.screen_size_spin = QSpinBox() self.screen_size_spin.setRange(100, 2000) self.screen_size_spin.setValue(int(self.config['screen_target_size'])) screen_layout.addWidget(self.screen_size_spin) layout.addWidget(screen_group) layout.addStretch() def create_move_settings(self, layout): group = QGroupBox("鼠标移动参数") group_layout = QVBoxLayout(group) # FOV设置 group_layout.addWidget(QLabel("横向FOV(度):")) self.fov_spin = QDoubleSpinBox() self.fov_spin.setRange(1, 179) self.fov_spin.setValue(float(self.config.get('move_fov_horizontal', '90'))) group_layout.addWidget(self.fov_spin) # 鼠标DPI group_layout.addWidget(QLabel("鼠标DPI:")) self.dpi_spin = QSpinBox() self.dpi_spin.setRange(100, 20000) self.dpi_spin.setValue(int(self.config.get('move_mouse_dpi', '400'))) group_layout.addWidget(self.dpi_spin) layout.addWidget(group) layout.addStretch() def create_target_settings(self, layout): group = QGroupBox("目标点偏移") group_layout = QVBoxLayout(group) # X轴偏移 - 添加百分比显示 group_layout.addWidget(QLabel("X轴偏移:")) x_layout = QHBoxLayout() self.x_offset_slider = QSlider(Qt.Orientation.Horizontal) self.x_offset_slider.setRange(0, 100) self.x_offset_slider.setValue(int(float(self.config.get('target_offset_x', '50')))) x_layout.addWidget(self.x_offset_slider) self.x_offset_value = QLabel(f"{int(float(self.config.get('target_offset_x', '50')))}%") self.x_offset_value.setFixedWidth(50) x_layout.addWidget(self.x_offset_value) group_layout.addLayout(x_layout) # 连接滑块值变化事件 self.x_offset_slider.valueChanged.connect(lambda value: self.x_offset_value.setText(f"{value}%")) # Y轴偏移 - 添加百分比显示 group_layout.addWidget(QLabel("Y轴偏移:")) y_layout = QHBoxLayout() self.y_offset_slider = QSlider(Qt.Orientation.Horizontal) self.y_offset_slider.setRange(0, 100) self.y_offset_slider.setValue(int(float(self.config.get('target_offset_y', '50')))) y_layout.addWidget(self.y_offset_slider) self.y_offset_value = QLabel(f"{int(float(self.config.get('target_offset_y', '50')))}%") self.y_offset_value.setFixedWidth(50) y_layout.addWidget(self.y_offset_value) group_layout.addLayout(y_layout) # 连接滑块值变化事件 self.y_offset_slider.valueChanged.connect(lambda value: self.y_offset_value.setText(f"{value}%")) # 说明 info_label = QLabel("(0% = 左上角, 50% = 中心, 100% = 右下角)") info_label.setStyleSheet("font-size: 9pt; color: #888888;") group_layout.addWidget(info_label) layout.addWidget(group) layout.addStretch() def create_pid_settings(self, layout): group = QGroupBox("PID参数") group_layout = QVBoxLayout(group) # Kp参数 group_layout.addWidget(QLabel("比例增益(Kp):")) kp_layout = QHBoxLayout() self.kp_slider = QSlider(Qt.Orientation.Horizontal) self.kp_slider.setRange(1, 1000) # 0.01到10.0,步长0.01 self.kp_slider.setValue(int(float(self.config.get('pid_kp', '1.0')) * 100)) kp_layout.addWidget(self.kp_slider) self.kp_value = QLabel(f"{float(self.config.get('pid_kp', '1.0')):.2f}") self.kp_value.setFixedWidth(50) kp_layout.addWidget(self.kp_value) group_layout.addLayout(kp_layout) # 连接滑块值变化事件 self.kp_slider.valueChanged.connect(lambda value: self.kp_value.setText(f"{value / 100:.2f}")) # Ki参数 group_layout.addWidget(QLabel("积分增益(Ki):")) ki_layout = QHBoxLayout() self.ki_slider = QSlider(Qt.Orientation.Horizontal) self.ki_slider.setRange(0, 500) # 0.0000到0.1000,步长0.001 self.ki_slider.setValue(int(float(self.config.get('pid_ki', '0.05')) * 10000)) ki_layout.addWidget(self.ki_slider) self.ki_value = QLabel(f"{float(self.config.get('pid_ki', '0.05')):.4f}") self.ki_value.setFixedWidth(50) ki_layout.addWidget(self.ki_value) group_layout.addLayout(ki_layout) # 连接滑块值变化事件 self.ki_slider.valueChanged.connect(lambda value: self.ki_value.setText(f"{value / 10000:.4f}")) # Kd参数 group_layout.addWidget(QLabel("微分增益(Kd):")) kd_layout = QHBoxLayout() self.kd_slider = QSlider(Qt.Orientation.Horizontal) self.kd_slider.setRange(0, 5000) # 0.000到5.000,步长0.001 self.kd_slider.setValue(int(float(self.config.get('pid_kd', '0.2')) * 1000)) kd_layout.addWidget(self.kd_slider) self.kd_value = QLabel(f"{float(self.config.get('pid_kd', '0.2')):.3f}") self.kd_value.setFixedWidth(50) kd_layout.addWidget(self.kd_value) group_layout.addLayout(kd_layout) # 连接滑块值变化事件 self.kd_slider.valueChanged.connect(lambda value: self.kd_value.setText(f"{value / 1000:.3f}")) # 说明 info_text = "建议调整顺序: Kp → Kd → Ki\n\n" \ "先调整Kp至响应迅速但不过冲\n" \ "再增加Kd抑制震荡\n" \ "最后微调Ki消除剩余误差" info_label = QLabel(info_text) info_label.setStyleSheet("font-size: 9pt; color: #888888;") group_layout.addWidget(info_label) layout.addWidget(group) layout.addStretch() # 创建贝塞尔曲线设置 def create_bezier_settings(self, layout): group = QGroupBox("贝塞尔曲线参数") group_layout = QVBoxLayout(group) # 步数设置 group_layout.addWidget(QLabel("步数:")) steps_layout = QHBoxLayout() self.steps_slider = QSlider(Qt.Orientation.Horizontal) self.steps_slider.setRange(1, 500) self.steps_slider.setValue(int(self.config.get('bezier_steps', 100))) steps_layout.addWidget(self.steps_slider) self.steps_value = QLabel(str(self.config.get('bezier_steps', 100))) self.steps_value.setFixedWidth(50) steps_layout.addWidget(self.steps_value) group_layout.addLayout(steps_layout) # 连接滑块值变化事件 self.steps_slider.valueChanged.connect(lambda value: self.steps_value.setText(str(value))) # 总移动时间设置 (秒) group_layout.addWidget(QLabel("总移动时间 (秒):")) duration_layout = QHBoxLayout() self.duration_slider = QSlider(Qt.Orientation.Horizontal) self.duration_slider.setRange(0, 100) # 0.01到1.0,步长0.01 self.duration_slider.setValue(int(float(self.config.get('bezier_duration', 0.1)) * 100)) duration_layout.addWidget(self.duration_slider) self.duration_value = QLabel(f"{float(self.config.get('bezier_duration', 0.1)):.2f}") self.duration_value.setFixedWidth(50) duration_layout.addWidget(self.duration_value) group_layout.addLayout(duration_layout) # 连接滑块值变化事件 self.duration_slider.valueChanged.connect(lambda value: self.duration_value.setText(f"{value / 100:.2f}")) # 控制点偏移幅度 group_layout.addWidget(QLabel("控制点偏移幅度 (0-1):")) curve_layout = QHBoxLayout() self.curve_slider = QSlider(Qt.Orientation.Horizontal) self.curve_slider.setRange(0, 100) # 0.00到1.00,步长0.01 self.curve_slider.setValue(int(float(self.config.get('bezier_curve', 0.3)) * 100)) curve_layout.addWidget(self.curve_slider) self.curve_value = QLabel(f"{float(self.config.get('bezier_curve', 0.3)):.2f}") self.curve_value.setFixedWidth(50) curve_layout.addWidget(self.curve_value) group_layout.addLayout(curve_layout) # 连接滑块值变化事件 self.curve_slider.valueChanged.connect(lambda value: self.curve_value.setText(f"{value / 100:.2f}")) # 说明 info_text = "贝塞尔曲线参数说明:\n\n" \ "• 步数: 鼠标移动的细分步数,值越大移动越平滑\n" \ "• 总移动时间: 鼠标移动的总时间,值越小移动越快\n" \ "• 控制点偏移幅度: 控制贝塞尔曲线的弯曲程度,0为直线,1为最大弯曲" info_label = QLabel(info_text) info_label.setStyleSheet("font-size: 9pt; color: #888888;") group_layout.addWidget(info_label) layout.addWidget(group) layout.addStretch() def save_config(self): try: # 保存配置到字典 model_name = self.model_combo.currentText() model_path = self.model_name_to_path.get(model_name, model_name) self.config['model_path'] = model_path self.config['model_device'] = self.device_combo.currentText() self.config['screen_monitor'] = str(self.monitor_spin.value()) self.config['screen_target_size'] = str(self.screen_size_spin.value()) # 检测参数 self.config['detection_conf_thres'] = str(self.conf_slider.value() / 100) self.config['detection_iou_thres'] = str(self.iou_slider.value() / 100) self.config['detection_classes'] = self.classes_edit.text() # 移动设置 self.config['move_fov_horizontal'] = str(self.fov_spin.value()) self.config['move_mouse_dpi'] = str(self.dpi_spin.value()) # 目标点偏移设置 self.config['target_offset_x'] = str(self.x_offset_slider.value()) self.config['target_offset_y'] = str(self.y_offset_slider.value()) # PID设置 self.config['pid_kp'] = str(self.kp_slider.value() / 100) self.config['pid_ki'] = str(self.ki_slider.value() / 10000) self.config['pid_kd'] = str(self.kd_slider.value() / 1000) # 贝塞尔曲线设置 self.config['bezier_steps'] = str(self.steps_slider.value()) self.config['bezier_duration'] = str(self.duration_slider.value() / 100) self.config['bezier_curve'] = str(self.curve_slider.value() / 100) # 保存为TXT格式 with open('detection_config.txt', 'w', encoding='utf-8') as f: for key, value in self.config.items(): f.write(f"{key} = {value}\n") # 检查需要重启的参数是否被修改 restart_required = False restart_params = [] # 比较模型路径是否变化 if self.config['model_path'] != self.original_config.get('model_path', ''): restart_required = True restart_params.append("模型路径") # 比较设备类型是否变化 if self.config['model_device'] != self.original_config.get('model_device', ''): restart_required = True restart_params.append("设备类型") # 比较屏幕区域大小是否变化 if self.config['screen_target_size'] != self.original_config.get('screen_target_size', ''): restart_required = True restart_params.append("屏幕区域大小") # 比较检测类别是否变化 if self.config['detection_classes'] != self.original_config.get('detection_classes', ''): restart_required = True restart_params.append("检测类别") # 动态更新检测器配置 if self.parent() and hasattr(self.parent(), 'detector'): success = self.parent().detector.update_config('detection_config.txt') if success: if restart_required: # 需要重启的参数已修改 param_list = "、".join(restart_params) QMessageBox.information( self, "配置已保存", f"配置已保存!以下参数需要重启才能生效:\n{param_list}\n\n" "其他参数已实时更新。" ) else: # 所有参数都已实时更新 QMessageBox.information(self, "成功", "配置已实时更新生效!") else: QMessageBox.warning(self, "部分更新", "配置更新失败,请查看日志") else: QMessageBox.information(self, "成功", "配置已保存!部分参数需重启生效") self.accept() except Exception as e: QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") if __name__ == "__main__": detector = ScreenDetector('detection_config.txt') print(f"\nDXcam检测器初始化完成 | 设备: {detector.device.upper()}") app = QApplication(sys.argv) # 设置全局样式 app.setStyle("Fusion") app.setStyleSheet(""" QWidget { background-color: #252526; color: #D4D4D4; selection-background-color: #0078D7; selection-color: white; } QPushButton { background-color: #0078D7; color: white; border: none; padding: 5px 10px; border-radius: 4px; } QPushButton:hover { background-color: #106EBE; } QPushButton:pressed { background-color: #005A9E; } QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QSlider { background-color: #3C3C40; color: #D4D4D4; border: 1px solid #3F3F46; border-radius: 4px; padding: 3px; } QComboBox:editable { background-color: #3C3C40; } QComboBox QAbstractItemView { background-color: #2D2D30; color: #D4D4D4; selection-background-color: #0078D7; selection-color: white; } QLabel { color: #D4D4D4; } QTabWidget::pane { border: 1px solid #3F3F46; background: #252526; } QTabBar::tab { background: #1E1E1E; color: #A0A0A0; padding: 8px 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background: #252526; color: #FFFFFF; border-bottom: 2px solid #0078D7; } QTabBar::tab:hover { background: #2D2D30; } QGroupBox { background-color: #252526; border: 1px solid #3F3F46; border-radius: 4px; margin-top: 1ex; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; background-color: transparent; color: #CCCCCC; } """) window = MainWindow(detector) window.show() sys.exit(app.exec()) 重构我的代码,减少不必要的代码,确保功能一样,UI一样。

import sys import os import fitz # PyMuPDF import pandas as pd import numpy as np import tempfile import shutil import re import time from datetime import datetime from PIL import Image, ImageDraw import cv2 import csv import json from collections import defaultdict from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog, QListWidget, QTableWidget, QTableWidgetItem, QAbstractItemView, QHeaderView, QLineEdit, QFrame, QSizePolicy, QProgressBar, QMessageBox, QComboBox, QGridLayout, QTextEdit, QDialog, QDialogButtonBox ) from PyQt5.QtCore import Qt, QSize, QTimer, QThread, pyqtSignal from PyQt5.QtGui import QIcon, QFont, QColor, QPixmap, QBrush, QPainter # 集成PaddleOCR try: from paddleocr import PaddleOCR except ImportError: print("PaddleOCR not installed. Please install with: pip install paddlepaddle paddleocr") sys.exit(1) class OCRWorker(QThread): progress_updated = pyqtSignal(int, str) extraction_complete = pyqtSignal(list, str) watermark_removed = pyqtSignal(str, str) def __init__(self, pdf_path, output_dir, watermark_text=None, parent=None): super().__init__(parent) self.pdf_path = pdf_path self.output_dir = output_dir self.watermark_text = watermark_text self.file_name = os.path.basename(pdf_path) self.canceled = False self.ocr = PaddleOCR(use_angle_cls=True, lang='en', use_gpu=True) def run(self): try: # 第一步:去除水印(如果需要) processed_path = self.pdf_path if self.watermark_text: processed_path = self.remove_watermark() if processed_path: self.watermark_removed.emit(processed_path, self.file_name) else: self.progress_updated.emit(100, "水印去除失败") return # 第二步:提取内容 self.extract_content(processed_path) except Exception as e: self.progress_updated.emit(100, f"处理失败: {str(e)}") def remove_watermark(self): """使用OCR检测并去除水印""" try: doc = fitz.open(self.pdf_path) new_doc = fitz.open() output_path = os.path.join(self.output_dir, f"processed_{self.file_name}") total_pages = len(doc) for page_num in range(total_pages): if self.canceled: return None self.progress_updated.emit(int(30 * page_num / total_pages), f"正在处理第 {page_num+1} 页水印") page = doc.load_page(page_num) pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) img_np = np.array(img) # 使用PaddleOCR检测文本 result = self.ocr.ocr(img_np, cls=True) # 检测水印文本 watermark_boxes = [] for line in result: for word_info in line: text = word_info[1][0] if self.watermark_text.lower() in text.lower(): box = word_info[0] # 将浮点坐标转换为整数 int_box = [(int(x), int(y)) for x, y in box] watermark_boxes.append(int_box) # 去除水印(用白色覆盖) if watermark_boxes: img_pil = Image.fromarray(img_np) draw = ImageDraw.Draw(img_pil) for box in watermark_boxes: # 创建覆盖矩形 min_x = min(point[0] for point in box) max_x = max(point[0] for point in box) min_y = min(point[1] for point in box) max_y = max(point[1] for point in box) # 扩展矩形范围确保完全覆盖 expand = 5 draw.rectangle( [min_x - expand, min_y - expand, max_x + expand, max_y + expand], fill=(255, 255, 255) img_np = np.array(img_pil) # 保存处理后的页面 img_bytes = Image.fromarray(img_np).tobytes() new_page = new_doc.new_page(width=img_np.shape[1], height=img_np.shape[0]) new_page.insert_image(fitz.Rect(0, 0, img_np.shape[1], img_np.shape[0]), stream=img_bytes) new_doc.save(output_path) new_doc.close() doc.close() return output_path except Exception as e: print(f"Error removing watermark: {e}") return None def extract_content(self, pdf_path): """使用PaddleOCR提取内容""" try: doc = fitz.open(pdf_path) extracted_data = [] total_pages = len(doc) for page_num in range(total_pages): if self.canceled: return self.progress_updated.emit(30 + int(70 * page_num / total_pages), f"正在提取第 {page_num+1} 页内容") page = doc.load_page(page_num) pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) img_np = np.array(img) # 使用PaddleOCR提取文本 result = self.ocr.ocr(img_np, cls=True) # 处理OCR结果 page_text = "" for line in result: line_text = " ".join([word_info[1][0] for word_info in line]) page_text += line_text + "\n" # 提取结构化数据(示例逻辑) extracted = self.extract_structured_data(page_text, page_num + 1) extracted_data.extend(extracted) doc.close() self.extraction_complete.emit(extracted_data, self.file_name) except Exception as e: self.progress_updated.emit(100, f"内容提取失败: {str(e)}") def extact_structured_data(self, text, page_num): """从文本中提取结构化数据(示例实现)""" extracted = [] # 提取发票信息 invoice_match = re.search(r'Invoice\s+Number\s*:\s*(\w+)', text, re.IGNORECASE) date_match = re.search(r'Date\s*:\s*(\d{2}/\d{2}/\d{4})', text, re.IGNORECASE) total_match = re.search(r'Total\s+Amount\s*:\s*([\d,]+\.\d{2})', text, re.IGNORECASE) if invoice_match or date_match or total_match: extracted.append({ "Document": self.file_name, "Page": page_num, "Type": "Invoice", "Invoice Number": invoice_match.group(1) if invoice_match else "N/A", "Date": date_match.group(1) if date_match else "N/A", "Amount": f"${total_match.group(1)}" if total_match else "N/A" }) # 提取报告信息 report_match = re.search(r'Report\s+Title\s*:\s*(.+)', text, re.IGNORECASE) author_match = re.search(r'Author\s*:\s*(.+)', text, re.IGNORECASE) if report_match or author_match: extracted.append({ "Document": self.file_name, "Page": page_num, "Type": "Report", "Report Title": report_match.group(1) if report_match else "N/A", "Author": author_match.group(1) if author_match else "N/A", "Summary": text[:200] + "..." if len(text) > 200 else text }) # 如果没有匹配到特定结构,返回整个页面文本 if not extracted: extracted.append({ "Document": self.file_name, "Page": page_num, "Type": "General", "Content": text }) return extracted def cancel(self): self.canceled = True class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("PDF智能处理工具") self.setGeometry(100, 100, 1200, 800) self.setMinimumSize(1000, 700) # 应用主色调 self.primary_color = "#2c3e50" self.secondary_color = "#3498db" self.accent_color = "#e67e22" self.light_color = "#ecf0f1" self.dark_color = "#34495e" # 初始化状态 self.current_files = [] self.extracted_data = [] self.history_data = [] self.ocr_worker = None self.temp_dir = tempfile.mkdtemp() # 创建中央部件和主布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) # 创建顶部栏 self.create_header(main_layout) # 创建主内容区域 self.content_stack = QStackedWidget() main_layout.addWidget(self.content_stack, 1) # 创建各个页面 self.home_page = self.create_home_page() self.upload_page = self.create_upload_page() self.history_page = self.create_history_page() self.analysis_page = self.create_analysis_page() self.content_stack.addWidget(self.home_page) self.content_stack.addWidget(self.upload_page) self.content_stack.addWidget(self.history_page) self.content_stack.addWidget(self.analysis_page) # 创建底部导航栏 self.create_footer(main_layout) # 模拟一些历史数据 self.simulate_history_data() # 设置首页为默认页面 self.content_stack.setCurrentIndex(0) self.update_home_page() def closeEvent(self, event): """清理临时文件""" try: shutil.rmtree(self.temp_dir, ignore_errors=True) except: pass event.accept() def simulate_history_data(self): """模拟一些历史数据用于展示""" for i in range(5): self.history_data.append({ "id": i, "file_name": f"document_{i+1}.pdf", "date": f"2023-0{i+1}-15", "status": "Completed", "pages": i+3, "type": "Invoice" if i % 2 == 0 else "Report", "extracted_data": [ { "Document": f"document_{i+1}.pdf", "Page": 1, "Type": "Invoice", "Invoice Number": f"INV-2023-{i+1:04d}", "Date": f"2023-0{i+1}-15", "Amount": f"${(i+1)*250:.2f}" } ] }) def create_header(self, main_layout): """创建应用头部""" header = QWidget() header.setStyleSheet(f"background-color: {self.primary_color}; padding: 15px;") header_layout = QHBoxLayout(header) header_layout.setContentsMargins(20, 10, 20, 10) # 应用标题 title_label = QLabel("PDF智能处理工具") title_label.setStyleSheet(f"color: white; font-size: 24px; font-weight: bold;") header_layout.addWidget(title_label) # 右侧用户区域 user_widget = QWidget() user_layout = QHBoxLayout(user_widget) user_layout.setSpacing(15) user_icon = QLabel() user_icon.setPixmap(self.create_icon("👤", 40)) user_layout.addWidget(user_icon) user_name = QLabel("管理员") user_name.setStyleSheet("color: white; font-size: 16px;") user_layout.addWidget(user_name) header_layout.addWidget(user_widget) main_layout.addWidget(header) def create_footer(self, main_layout): """创建底部导航栏""" footer = QWidget() footer.setStyleSheet(f"background-color: {self.dark_color};") footer.setFixedHeight(60) footer_layout = QHBoxLayout(footer) footer_layout.setContentsMargins(0, 0, 0, 0) footer_layout.setSpacing(0) # 导航按钮 nav_items = [ ("首页", "home", 0), ("上传与提取", "upload", 1), ("历史记录", "history", 2), ("数据分析", "analysis", 3) ] for text, icon_name, index in nav_items: btn = QPushButton(text) btn.setIcon(QIcon(self.create_icon("🏠" if icon_name=="home" else "📤" if icon_name=="upload" else "📋" if icon_name=="history" else "📊", 24))) btn.setIconSize(QSize(24, 24)) btn.setFixedHeight(60) btn.setStyleSheet(f""" QPushButton {{ color: {self.light_color}; font-size: 14px; font-weight: bold; border: none; background-color: {self.dark_color}; }} QPushButton:hover {{ background-color: {self.primary_color}; }} """) btn.clicked.connect(lambda _, idx=index: self.navigate_to(idx)) footer_layout.addWidget(btn) main_layout.addWidget(footer) def create_icon(self, emoji, size=24): """创建表情符号图标""" pixmap = QPixmap(size, size) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) painter.setFont(QFont("Arial", size - 4)) painter.drawText(pixmap.rect(), Qt.AlignCenter, emoji) painter.end() return pixmap def navigate_to(self, index): """导航到指定页面""" self.content_stack.setCurrentIndex(index) # 更新页面内容 if index == 0: # 首页 self.update_home_page() elif index == 1: # 上传与提取 pass elif index == 2: # 历史记录 self.update_history_page() elif index == 3: # 数据分析 self.update_analysis_page() def create_home_page(self): """创建首页""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 欢迎卡片 welcome_card = QFrame() welcome_card.setStyleSheet(f""" QFrame {{ background-color: white; border-radius: 10px; padding: 20px; }} """) welcome_layout = QVBoxLayout(welcome_card) welcome_title = QLabel("欢迎使用PDF智能处理工具") welcome_title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") welcome_layout.addWidget(welcome_title) welcome_text = QLabel("本工具提供PDF水印去除、内容提取和数据分析功能,支持批量处理PDF文件,快速提取结构化数据并导出为Excel或CSV格式。") welcome_text.setStyleSheet("font-size: 16px; color: #7f8c8d;") welcome_text.setWordWrap(True) welcome_layout.addWidget(welcome_text) # OCR信息 ocr_info = QLabel("当前使用PaddleOCR引擎,支持中英文识别") ocr_info.setStyleSheet("font-size: 14px; color: #3498db; font-weight: bold;") welcome_layout.addWidget(ocr_info) layout.addWidget(welcome_card) # 最近上传区域 recent_label = QLabel("最近上传") recent_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #2c3e50;") layout.addWidget(recent_label) # 最近上传列表 self.recent_list = QListWidget() self.recent_list.setStyleSheet(""" QListWidget { background-color: white; border-radius: 10px; border: 1px solid #ddd; } QListWidget::item { padding: 15px; border-bottom: 1px solid #eee; } QListWidget::item:selected { background-color: #e6f7ff; } """) self.recent_list.setAlternatingRowColors(True) layout.addWidget(self.recent_list, 1) # 快捷操作按钮 quick_actions = QWidget() quick_layout = QHBoxLayout(quick_actions) quick_layout.setSpacing(15) actions = [ ("上传新文件", "upload", self.navigate_to_upload), ("查看历史记录", "history", lambda: self.navigate_to(2)), ("数据分析", "analysis", lambda: self.navigate_to(3)) ] for text, icon_name, action in actions: btn = QPushButton(text) btn.setIcon(QIcon(self.create_icon("📤" if icon_name=="upload" else "📋" if icon_name=="history" else "📊", 24))) btn.setIconSize(QSize(24, 24)) btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; font-size: 16px; font-weight: bold; padding: 12px 20px; border-radius: 8px; }} QPushButton:hover {{ background-color: #2980b9; }} """) btn.clicked.connect(action) quick_layout.addWidget(btn) layout.addWidget(quick_actions) return page def update_home_page(self): """更新首页内容""" self.recent_list.clear() # 显示最近的5条记录 for item in self.history_data[:5]: list_item = QLabel(f""" {item['file_name']} 上传时间: {item['date']} | 状态: {item['status']} | 类型: {item['type']} """) list_widget = QListWidgetItem(self.recent_list) list_widget.setSizeHint(list_item.sizeHint()) self.recent_list.addItem(list_widget) self.recent_list.setItemWidget(list_widget, list_item) def navigate_to_upload(self): """导航到上传页面""" self.content_stack.setCurrentIndex(1) def create_upload_page(self): """创建上传与提取页面""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 标题 title = QLabel("上传与提取") title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") layout.addWidget(title) # 上传区域 upload_card = QFrame() upload_card.setStyleSheet(f""" QFrame {{ background-color: white; border-radius: 10px; border: 2px dashed {self.secondary_color}; padding: 40px; }} """) upload_layout = QVBoxLayout(upload_card) upload_layout.setAlignment(Qt.AlignCenter) upload_icon = QLabel() upload_icon.setPixmap(self.create_icon("📤", 80)) upload_icon.setAlignment(Qt.AlignCenter) upload_layout.addWidget(upload_icon) upload_text = QLabel("拖放PDF文件到此处,或点击选择文件") upload_text.setStyleSheet("font-size: 18px; color: #7f8c8d; margin-top: 20px;") upload_text.setAlignment(Qt.AlignCenter) upload_layout.addWidget(upload_text) upload_btn = QPushButton("选择PDF文件") upload_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; font-size: 16px; padding: 10px 20px; border-radius: 5px; margin-top: 20px; }} QPushButton:hover {{ background-color: #2980b9; }} """) upload_btn.clicked.connect(self.select_pdf_files) upload_layout.addWidget(upload_btn, alignment=Qt.AlignCenter) # 水印选项 watermark_layout = QHBoxLayout() watermark_layout.setSpacing(10) watermark_label = QLabel("水印文本(可选):") watermark_label.setStyleSheet("font-size: 14px;") watermark_layout.addWidget(watermark_label) self.watermark_input = QLineEdit() self.watermark_input.setPlaceholderText("输入要删除的水印文本") self.watermark_input.setStyleSheet("padding: 5px; border: 1px solid #ddd; border-radius: 3px;") watermark_layout.addWidget(self.watermark_input, 1) upload_layout.addLayout(watermark_layout) layout.addWidget(upload_card, 1) # 进度区域 progress_layout = QVBoxLayout() progress_layout.setSpacing(10) progress_label = QLabel("处理进度") progress_label.setStyleSheet("font-size: 18px; font-weight: bold; color: #2c3e50;") progress_layout.addWidget(progress_label) self.progress_bar = QProgressBar() self.progress_bar.setStyleSheet(""" QProgressBar { border: 1px solid #ddd; border-radius: 5px; text-align: center; height: 25px; } QProgressBar::chunk { background-color: #3498db; width: 10px; } """) progress_layout.addWidget(self.progress_bar) self.progress_text = QLabel("等待处理文件...") self.progress_text.setStyleSheet("font-size: 14px; color: #7f8c8d;") progress_layout.addWidget(self.progress_text) # 取消按钮 self.cancel_btn = QPushButton("取消处理") self.cancel_btn.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; padding: 8px 20px; border-radius: 5px; font-weight: bold; } QPushButton:hover { background-color: #c0392b; } """) self.cancel_btn.clicked.connect(self.cancel_processing) self.cancel_btn.setVisible(False) progress_layout.addWidget(self.cancel_btn, alignment=Qt.AlignRight) layout.addLayout(progress_layout) return page def select_pdf_files(self): """选择PDF文件""" files, _ = QFileDialog.getOpenFileNames( self, "选择PDF文件", "", "PDF文件 (*.pdf)" ) if files: self.current_files = files self.process_files() def process_files(self): """处理选中的文件""" if not self.current_files: return # 重置状态 self.extracted_data = [] # 显示取消按钮 self.cancel_btn.setVisible(True) # 处理第一个文件 file_path = self.current_files[0] file_name = os.path.basename(file_path) # 获取水印文本 watermark_text = self.watermark_input.text().strip() or None # 创建工作线程 self.ocr_worker = OCRWorker( file_path, self.temp_dir, watermark_text ) # 连接信号 self.ocr_worker.progress_updated.connect(self.update_progress) self.ocr_worker.watermark_removed.connect(self.on_watermark_removed) self.ocr_worker.extraction_complete.connect(self.on_extraction_complete) # 开始处理 self.ocr_worker.start() def update_progress(self, progress, message): """更新处理进度""" self.progress_bar.setValue(progress) self.progress_text.setText(message) def on_watermark_removed(self, output_path, file_name): """水印去除完成""" self.progress_text.setText(f"水印已移除: {file_name}") def on_extraction_complete(self, extracted_data, file_name): """内容提取完成""" self.extracted_data.extend(extracted_data) # 保存到历史记录 self.history_data.insert(0, { "id": len(self.history_data), "file_name": file_name, "date": datetime.now().strftime("%Y-%m-%d %H:%M"), "status": "Completed", "pages": len(extracted_data), "type": "Invoice" if any(d['Type'] == 'Invoice' for d in extracted_data) else "Report", "extracted_data": extracted_data }) # 显示预览 self.show_preview(extracted_data, file_name) # 处理下一个文件(如果有) if len(self.current_files) > 1: self.current_files.pop(0) self.process_files() else: self.current_files = [] self.cancel_btn.setVisible(False) def cancel_processing(self): """取消处理""" if self.ocr_worker and self.ocr_worker.isRunning(): self.ocr_worker.cancel() self.ocr_worker.wait() self.progress_text.setText("处理已取消") self.cancel_btn.setVisible(False) def show_preview(self, data, file_name): """显示预览窗口""" # 创建预览对话框 preview_dialog = QDialog(self) preview_dialog.setWindowTitle(f"预览 - {file_name}") preview_dialog.resize(1000, 700) layout = QVBoxLayout(preview_dialog) layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(15) # 标题 title = QLabel(f"文件内容提取结果: {file_name}") title.setStyleSheet("font-size: 20px; font-weight: bold; color: #2c3e50;") layout.addWidget(title) # OCR引擎信息 ocr_info = QLabel("使用PaddleOCR引擎提取内容") ocr_info.setStyleSheet("font-size: 14px; color: #3498db; font-weight: bold;") layout.addWidget(ocr_info) # 数据表格 if data: table = QTableWidget() table.setRowCount(len(data)) # 获取所有可能的列 all_columns = set() for item in data: all_columns.update(item.keys()) columns = sorted(all_columns) table.setColumnCount(len(columns)) table.setHorizontalHeaderLabels(columns) # 填充数据 for row_idx, row_data in enumerate(data): for col_idx, col_name in enumerate(columns): value = str(row_data.get(col_name, "")) item = QTableWidgetItem(value) table.setItem(row_idx, col_idx, item) # 表格样式 table.setStyleSheet(""" QTableWidget { background-color: white; border: 1px solid #ddd; gridline-color: #eee; } QHeaderView::section { background-color: #f8f9fa; padding: 8px; border: none; font-weight: bold; } """) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked) table.setSelectionMode(QAbstractItemView.SingleSelection) table.setSelectionBehavior(QAbstractItemView.SelectRows) layout.addWidget(table, 1) else: no_data_label = QLabel("未提取到有效数据") no_data_label.setStyleSheet("font-size: 16px; color: #7f8c8d;") no_data_label.setAlignment(Qt.AlignCenter) layout.addWidget(no_data_label, 1) # 操作按钮 btn_layout = QHBoxLayout() btn_layout.setSpacing(15) export_excel_btn = QPushButton("导出为Excel") export_excel_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} QPushButton:hover {{ background-color: #2980b9; }} """) export_excel_btn.clicked.connect(self.export_to_excel) btn_layout.addWidget(export_excel_btn) export_csv_btn = QPushButton("导出为CSV") export_csv_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.accent_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} QPushButton:hover {{ background-color: #d35400; }} """) export_csv_btn.clicked.connect(self.export_to_csv) btn_layout.addWidget(export_csv_btn) close_btn = QPushButton("关闭") close_btn.setStyleSheet(""" QPushButton { background-color: #95a5a6; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; } QPushButton:hover { background-color: #7f8c8d; } """) close_btn.clicked.connect(preview_dialog.accept) btn_layout.addWidget(close_btn) layout.addLayout(btn_layout) preview_dialog.exec_() def export_to_excel(self): """导出为Excel""" if not self.extracted_data: QMessageBox.warning(self, "导出失败", "没有可导出的数据") return file_path, _ = QFileDialog.getSaveFileName( self, "保存Excel文件", "", "Excel文件 (*.xlsx)" ) if file_path: if not file_path.endswith('.xlsx'): file_path += '.xlsx' try: # 将数据转换为DataFrame df = pd.DataFrame(self.extracted_data) # 导出到Excel df.to_excel(file_path, index=False) QMessageBox.information(self, "导出成功", f"数据已成功导出到: {file_path}") except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") def export_to_csv(self): """导出为CSV""" if not self.extracted_data: QMessageBox.warning(self, "导出失败", "没有可导出的数据") return file_path, _ = QFileDialog.getSaveFileName( self, "保存CSV文件", "", "CSV文件 (*.csv)" ) if file_path: if not file_path.endswith('.csv'): file_path += '.csv' try: # 获取所有可能的字段 all_fields = set() for item in self.extracted_data: all_fields.update(item.keys()) # 写入CSV with open(file_path, 'w', newline='', encoding='utf-8') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=sorted(all_fields)) writer.writeheader() writer.writerows(self.extracted_data) QMessageBox.information(self, "导出成功", f"数据已成功导出到: {file_path}") except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") def create_history_page(self): """创建历史记录页面""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 标题和搜索 header_layout = QHBoxLayout() title = QLabel("历史记录") title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") header_layout.addWidget(title) search_layout = QHBoxLayout() search_layout.setSpacing(10) self.search_input = QLineEdit() self.search_input.setPlaceholderText("搜索文件名...") self.search_input.setStyleSheet(""" QLineEdit { padding: 8px 15px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; } """) self.search_input.setFixedWidth(300) self.search_input.returnPressed.connect(self.search_history) search_layout.addWidget(self.search_input) search_btn = QPushButton("搜索") search_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 8px 20px; border-radius: 5px; }} """) search_btn.clicked.connect(self.search_history) search_layout.addWidget(search_btn) header_layout.addLayout(search_layout) layout.addLayout(header_layout) # 历史记录表格 self.history_table = QTableWidget() self.history_table.setColumnCount(5) self.history_table.setHorizontalHeaderLabels(["文件名", "上传时间", "状态", "页数", "类型"]) self.history_table.setStyleSheet(""" QTableWidget { background-color: white; border: 1px solid #ddd; gridline-color: #eee; } QHeaderView::section { background-color: #f8f9fa; padding: 12px; border: none; font-weight: bold; } """) self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.history_table.verticalHeader().setVisible(False) self.history_table.setSelectionBehavior(QAbstractItemView.SelectRows) self.history_table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.history_table.setSortingEnabled(True) layout.addWidget(self.history_table, 1) # 操作按钮 btn_layout = QHBoxLayout() btn_layout.setSpacing(15) view_btn = QPushButton("查看详情") view_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) view_btn.clicked.connect(self.view_history_detail) btn_layout.addWidget(view_btn) export_btn = QPushButton("导出记录") export_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.accent_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) export_btn.clicked.connect(self.export_history) btn_layout.addWidget(export_btn) delete_btn = QPushButton("删除记录") delete_btn.setStyleSheet(""" QPushButton { background-color: #e74c3c; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; } """) delete_btn.clicked.connect(self.delete_history) btn_layout.addWidget(delete_btn) layout.addLayout(btn_layout) return page def update_history_page(self): """更新历史记录页面""" self.history_table.setRowCount(len(self.history_data)) for row_idx, item in enumerate(self.history_data): self.history_table.setItem(row_idx, 0, QTableWidgetItem(item["file_name"])) self.history_table.setItem(row_idx, 1, QTableWidgetItem(item["date"])) status_item = QTableWidgetItem(item["status"]) if item["status"] == "Completed": status_item.setForeground(QBrush(QColor("#27ae60"))) else: status_item.setForeground(QBrush(QColor("#e74c3c"))) self.history_table.setItem(row_idx, 2, status_item) self.history_table.setItem(row_idx, 3, QTableWidgetItem(str(item["pages"]))) self.history_table.setItem(row_idx, 4, QTableWidgetItem(item["type"])) def search_history(self): """搜索历史记录""" search_text = self.search_input.text().lower() if not search_text: self.update_history_page() return filtered_data = [item for item in self.history_data if search_text in item["file_name"].lower()] self.history_table.setRowCount(len(filtered_data)) for row_idx, item in enumerate(filtered_data): self.history_table.setItem(row_idx, 0, QTableWidgetItem(item["file_name"])) self.history_table.setItem(row_idx, 1, QTableWidgetItem(item["date"])) status_item = QTableWidgetItem(item["status"]) if item["status"] == "Completed": status_item.setForeground(QBrush(QColor("#27ae60"))) else: status_item.setForeground(QBrush(QColor("#e74c3c"))) self.history_table.setItem(row_idx, 2, status_item) self.history_table.setItem(row_idx, 3, QTableWidgetItem(str(item["pages"]))) self.history_table.setItem(row_idx, 4, QTableWidgetItem(item["type"])) def view_history_detail(self): """查看历史记录详情""" selected_row = self.history_table.currentRow() if selected_row >= 0: file_name = self.history_table.item(selected_row, 0).text() history_item = next((item for item in self.history_data if item["file_name"] == file_name), None) if history_item: self.show_preview(history_item["extracted_data"], file_name) else: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") def export_history(self): """导出历史记录""" if not self.history_data: QMessageBox.warning(self, "导出失败", "没有可导出的历史记录") return file_path, _ = QFileDialog.getSaveFileName( self, "保存历史记录", "", "CSV文件 (*.csv)" ) if file_path: if not file_path.endswith('.csv'): file_path += '.csv' try: with open(file_path, 'w', newline='', encoding='utf-8') as csvfile: fieldnames = ['id', 'file_name', 'date', 'status', 'pages', 'type'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for item in self.history_data: writer.writerow({ 'id': item['id'], 'file_name': item['file_name'], 'date': item['date'], 'status': item['status'], 'pages': item['pages'], 'type': item['type'] }) QMessageBox.information(self, "导出成功", f"历史记录已成功导出到: {file_path}") except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") def delete_history(self): """删除历史记录""" selected_row = self.history_table.currentRow() if selected_row >= 0: file_name = self.history_table.item(selected_row, 0).text() reply = QMessageBox.question( self, '确认删除', f"确定要删除 '{file_name}' 的记录吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: self.history_data = [item for item in self.history_data if item['file_name'] != file_name] self.update_history_page() else: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") def create_analysis_page(self): """创建数据分析页面""" page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(30, 20, 30, 20) layout.setSpacing(20) # 标题 title = QLabel("数据分析") title.setStyleSheet("font-size: 24px; font-weight: bold; color: #2c3e50;") layout.addWidget(title) # 选择历史记录 select_layout = QHBoxLayout() select_label = QLabel("选择历史记录:") select_label.setStyleSheet("font-size: 16px;") select_layout.addWidget(select_label) self.history_combo = QComboBox() self.history_combo.setFixedWidth(300) self.history_combo.setStyleSheet("padding: 5px; border: 1px solid #ddd; border-radius: 3px;") select_layout.addWidget(self.history_combo) analyze_btn = QPushButton("分析数据") analyze_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 8px 20px; border-radius: 5px; }} """) analyze_btn.clicked.connect(self.analyze_data) select_layout.addWidget(analyze_btn) layout.addLayout(select_layout) # 图表区域 chart_container = QWidget() chart_layout = QGridLayout(chart_container) # 图表1 self.chart1_label = QLabel() self.chart1_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart1_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart1_label, 0, 0) # 图表2 self.chart2_label = QLabel() self.chart2_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart2_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart2_label, 0, 1) # 图表3 self.chart3_label = QLabel() self.chart3_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart3_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart3_label, 1, 0) # 图表4 self.chart4_label = QLabel() self.chart4_label.setStyleSheet("background-color: white; border-radius: 10px; padding: 10px;") self.chart4_label.setAlignment(Qt.AlignCenter) chart_layout.addWidget(self.chart4_label, 1, 1) layout.addWidget(chart_container, 1) # 操作按钮 btn_layout = QHBoxLayout() btn_layout.setSpacing(15) refresh_btn = QPushButton("刷新数据") refresh_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.secondary_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) refresh_btn.clicked.connect(self.update_analysis_page) btn_layout.addWidget(refresh_btn) export_btn = QPushButton("导出报告") export_btn.setStyleSheet(f""" QPushButton {{ background-color: {self.accent_color}; color: white; padding: 10px 20px; border-radius: 5px; font-weight: bold; }} """) export_btn.clicked.connect(self.export_report) btn_layout.addWidget(export_btn) layout.addLayout(btn_layout) return page def update_analysis_page(self): """更新数据分析页面""" # 更新历史记录选择框 self.history_combo.clear() for item in self.history_data: self.history_combo.addItem(f"{item['file_name']} - {item['date']}", item) # 显示初始图表 self.show_chart(self.chart1_label, "📊", "数据统计") self.show_chart(self.chart2_label, "📈", "趋势分析") self.show_chart(self.chart3_label, "📉", "比较分析") self.show_chart(self.chart4_label, "🧮", "类型分布") def show_chart(self, label, emoji, title): """显示模拟图表""" pixmap = QPixmap(400, 250) pixmap.fill(Qt.white) painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) # 绘制标题 painter.setFont(QFont("Arial", 14, QFont.Bold)) painter.drawText(pixmap.rect().adjusted(0, 10, 0, 0), Qt.AlignTop | Qt.AlignHCenter, title) # 绘制图表图标 painter.setFont(QFont("Arial", 80)) painter.drawText(pixmap.rect(), Qt.AlignCenter, emoji) # 绘制边框 painter.setPen(QColor("#ddd")) painter.drawRect(pixmap.rect().adjusted(0, 0, -1, -1)) painter.end() label.setPixmap(pixmap) def analyze_data(self): """分析数据""" if self.history_combo.currentIndex() < 0: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") return selected_item = self.history_combo.currentData() extracted_data = selected_item.get('extracted_data', []) if not extracted_data: QMessageBox.warning(self, "分析失败", "所选记录没有可分析的数据") return # 在实际应用中,这里会使用真实的数据分析逻辑 # 这里仅显示消息 QMessageBox.information( self, "数据分析", f"已对 '{selected_item['file_name']}' 进行数据分析\n" f"包含 {len(extracted_data)} 条记录" ) def export_report(self): """导出分析报告""" if self.history_combo.currentIndex() < 0: QMessageBox.warning(self, "选择记录", "请先选择一条历史记录") return selected_item = self.history_combo.currentData() file_path, _ = QFileDialog.getSaveFileName( self, "保存分析报告", "", "PDF文件 (*.pdf)" ) if file_path: if not file_path.endswith('.pdf'): file_path += '.pdf' try: # 在实际应用中,这里会生成真实的PDF报告 # 这里仅模拟导出 time.sleep(1) # 模拟生成报告的时间 QMessageBox.information( self, "导出成功", f"分析报告已成功导出到: {file_path}\n" f"包含对 '{selected_item['file_name']}' 的分析结果" ) except Exception as e: QMessageBox.critical(self, "导出失败", f"导出过程中发生错误: {str(e)}") if __name__ == "__main__": # 设置应用程序 app = QApplication(sys.argv) # 设置应用程序样式 app.setStyle("Fusion") # 创建并显示主窗口 window = MainWindow() window.show() # 执行应用程序 sys.exit(app.exec_())将数据分析功能改为接入本地部署的AI大模型进行分析和绘制图表

import sys import json from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLineEdit, QPushButton, QLabel, QSplitter, QListWidget, QStatusBar, QMessageBox ) from PyQt5.QtGui import QFont, QPalette, QColor, QTextCursor from PyQt5.QtCore import Qt, QThread, pyqtSignal import requests import base64 import hmac import hashlib import urllib.parse from datetime import datetime, timezone # 火山引擎 API 配置 API_URL = "https://open.volcengineapi.com/api/v3/chat/completions?Action=Chat&Version=2023-08-01" ACCESS_KEY = "AKLTMjA2YWNlYmIwNDAyNGMxOThkMjBkNTQxNjEwMmFhNzA" SECRET_KEY = "TmpJeU9XTTVaVEUwTlROak5HWmhaamcyTldZNVpqVTFaR1kxWXpZek5HVQ==" class VolcEngineWorker(QThread): """后台线程处理火山引擎 API 调用""" response_received = pyqtSignal(str, bool) # 信号:回复内容, 是否错误 status_update = pyqtSignal(str) # 状态更新信号 def __init__(self, prompt, parent=None): super().__init__(parent) self.prompt = prompt def run(self): """线程主函数""" try: self.status_update.emit("正在生成回复...") response = self.call_volcengine_api(self.prompt) self.response_received.emit(response, False) except Exception as e: self.response_received.emit(f"错误: {str(e)}", True) def generate_signature(self, secret_key, method, path, query_params, date): """生成火山引擎 API 签名""" sorted_keys = sorted(query_params.keys()) canonical_query = "&".join( f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote([0], safe='')}" for k in sorted_keys ) signature_origin = ( f"{method} {path} HTTP/1.1\n" f"Host: open.volcengineapi.com\n" f"Date: {date}\n" f"{canonical_query}" ) decoded_secret = base64.b64decode(secret_key) signature = hmac.new( decoded_secret, signature_origin.encode('utf-8'), hashlib.sha256 ).digest() return base64.b64encode(signature).decode() def call_volcengine_api(self, prompt): """调用火山引擎聊天 API""" messages = [ {"role": "system", "content": "你是有帮助的助手"}, {"role": "user", "content": prompt} ] request_data = { "messages": messages, "parameters": { "model": "skylark-lite-public", "temperature": 0.5, "max_tokens": 1024 } } date = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT") parsed_url = urllib.parse.urlparse(API_URL) query_params = urllib.parse.parse_qs(parsed_url.query) signature = self.generate_signature( SECRET_KEY, "POST", parsed_url.path, query_params, date ) auth_data = { "access_key": ACCESS_KEY, "algorithm": "HMAC-SHA256", "headers": "host date", "signature": signature } authorization = base64.b64encode(json.dumps(auth_data).encode()).decode() headers = { "Authorization": authorization, "Date": date, "Host": "open.volcengineapi.com", "Content-Type": "application/json" } response = requests.post( API_URL, headers=headers, json=request_data, timeout=30 ) if response.status_code == 200: data = response.json() return data.get("choices", [{}])[0].get("message", {}).get("content", "") else: return f"API请求失败: {response.status_code} - {response.text}" class ChatApplication(QMainWindow): """火山引擎聊天应用主界面""" def __init__(self): super().__init__() self.init_ui() self.setWindowTitle("火山引擎聊天助手") self.resize(800, 600) # 聊天历史 self.chat_history = [] def init_ui(self): """初始化用户界面""" # 主窗口部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 分割布局 splitter = QSplitter(Qt.Horizontal) main_layout.addWidget(splitter) # 左侧面板 - 聊天历史 left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(0, 0, 0, 0) history_label = QLabel("聊天历史") history_label.setFont(QFont("Arial", 10, QFont.Bold)) history_label.setStyleSheet("padding: 5px; background: #f0f0f0;") left_layout.addWidget(history_label) self.history_list = QListWidget() self.history_list.setStyleSheet(""" QListWidget { background-color: #f8f8f8; border: none; } QListWidget::item { padding: 8px; border-bottom: 1px solid #e0e0e0; } QListWidget::item:selected { background-color: #e0f0ff; } """) self.history_list.itemClicked.connect(self.load_chat_history) left_layout.addWidget(self.history_list) splitter.addWidget(left_panel) # 右侧面板 - 聊天区域 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(0, 0, 0, 0) # 聊天显示区域 self.chat_display = QTextEdit() self.chat_display.setReadOnly(True) self.chat_display.setStyleSheet(""" QTextEdit { background-color: #ffffff; border: none; padding: 10px; font-size: 14px; } """) self.chat_display.setFont(QFont("Arial", 12)) right_layout.addWidget(self.chat_display) # 输入区域 input_layout = QHBoxLayout() self.input_field = QLineEdit() self.input_field.setPlaceholderText("输入消息...") self.input_field.setStyleSheet(""" QLineEdit { padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; } """) self.input_field.returnPressed.connect(self.send_message) input_layout.addWidget(self.input_field) self.send_button = QPushButton("发送") self.send_button.setStyleSheet(""" QPushButton { background-color: #4a90e2; color: white; padding: 10px 20px; border: none; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #3a7bc8; } QPushButton:disabled { background-color: #cccccc; } """) self.send_button.clicked.connect(self.send_message) input_layout.addWidget(self.send_button) right_layout.addLayout(input_layout) splitter.addWidget(right_panel) # 设置分割比例 splitter.setSizes([200, 600]) # 状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage("就绪") # 添加初始消息 self.add_message("系统", "欢迎使用火山引擎聊天助手!请输入您的问题开始对话。", False) def add_message(self, sender, message, is_user=True): """添加消息到聊天显示区域""" # 格式化消息 if is_user: html = f""" {sender} {message} """ else: html = f""" {sender} {message} """ # 添加消息到聊天显示区域 self.chat_display.append(html) self.chat_display.moveCursor(QTextCursor.End) # 添加到聊天历史 self.chat_display.append({"sender": sender, "message": message, "is_user": is_user}) def send_message(self): """发送用户消息""" message = self.input_field.text().strip() if not message: return # 添加用户消息 self.add_message("您", message, True) # 清空输入框 self.input_field.clear() # 禁用发送按钮 self.send_button.setEnabled(False) self.input_field.setEnabled(False) # 创建并启动工作线程 self.worker = VolcEngineWorker(message) self.worker.response_received.connect(self.handle_api_response) self.worker.status_update.connect(self.status_bar.showMessage) self.worker.start() def handle_api_response(self, response, is_error): """处理 API 响应""" # 启用发送按钮 self.send_button.setEnabled(True) self.input_field.setEnabled(True) if is_error: self.add_message("系统", response, False) QMessageBox.critical(self, "错误", f"发生错误: {response}") else: self.add_message("火山引擎", response, False) # 添加到历史列表 self.history_list.addItem(f"对话 {len(self.chat_history) // 2 + 1}") self.status_bar.showMessage("就绪") def load_chat_history(self, item): """加载选中的聊天历史""" index = self.history_list.row(item) # 计算在历史记录中的位置 start_index = index * 2 # 每个对话包含2条消息 # 清空当前聊天显示 self.chat_display.clear() # 添加初始欢迎消息 self.chat_display.append(""" 系统 欢迎使用火山引擎聊天助手!以下是您选择的对话历史。 """) # 添加选中的历史消息 for i in range(start_index, start_index + 2): if i < len(self.chat_history): msg = self.chat_history[i] self.add_message(msg["sender"], msg["message"], msg["is_user"]) if __name__ == "__main__": app = QApplication(sys.argv) # 设置应用样式 app.setStyle("Fusion") palette = QPalette() palette.setColor(QPalette.Window, QColor(240, 240, 240)) palette.setColor(QPalette.WindowText, QColor(0, 0, 0)) app.setPalette(palette) window = ChatApplication() window.show() sys.exit(app.exec_()) append(self, text: Optional[str]): argument 1 has unexpected type 'dict'

修改代码,将音量分析修改为仅针对客服部分,注意修改后的整体变化: import os import sys import time import json import traceback import numpy as np import pandas as pd import torch import librosa import jieba import tempfile from pydub import AudioSegment from transformers import pipeline, AutoModelForSequenceClassification, AutoTokenizer from pyannote.audio import Pipeline from concurrent.futures import ThreadPoolExecutor, as_completed from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QTextEdit, QProgressBar, QGroupBox, QCheckBox, QListWidget, QMessageBox) from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer from PyQt5.QtGui import QFont from docx import Document from docx.shared import Inches import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from collections import Counter # 全局配置 MODEL_CONFIG = { "whisper_model": "openai/whisper-small", "diarization_model": "pyannote/[email protected]", # 使用更轻量模型 "sentiment_model": "IDEA-CCNL/Erlangshen-Roberta-110M-Sentiment", "chunk_size": 10, # 强制10秒分块 "sample_rate": 16000, "device": "cuda" if torch.cuda.is_available() else "cpu", "max_workers": 2 if torch.cuda.is_available() else 4, # GPU模式下并行度降低 "batch_size": 8 # 批处理大小 } # 初始化分词器 jieba.initialize() class ModelLoader(QThread): """模型加载线程""" progress = pyqtSignal(str) finished = pyqtSignal(bool, str) def __init__(self): super().__init__() self.models = {} self.error = None def run(self): try: self.progress.emit("正在加载语音识别模型...") # 语音识别模型 - 指定语言为中文减少推理时间 self.models["asr_pipeline"] = pipeline( "automatic-speech-recognition", model=MODEL_CONFIG["whisper_model"], torch_dtype=torch.float16, device=MODEL_CONFIG["device"], batch_size=MODEL_CONFIG["batch_size"], language="chinese" # 指定语言减少推理时间 ) self.progress.emit("正在加载说话人分离模型...") # 说话人分离模型 - 使用更轻量版本 self.models["diarization_pipeline"] = Pipeline.from_pretrained( MODEL_CONFIG["diarization_model"], use_auth_token=True ).to(torch.device(MODEL_CONFIG["device"]), torch.float16) self.progress.emit("正在加载情感分析模型...") # 情感分析模型 self.models["sentiment_tokenizer"] = AutoTokenizer.from_pretrained( MODEL_CONFIG["sentiment_model"] ) self.models["sentiment_model"] = AutoModelForSequenceClassification.from_pretrained( MODEL_CONFIG["sentiment_model"], torch_dtype=tor极狐float16 ).to(MODEL_CONFIG["device"]) self.finished.emit(True, "模型加载完成!") except Exception as e: self.error = str(e) traceback.print_exc() self.finished.emit(False, f"模型加载失败: {str(e)}") class AudioAnalyzer: """深度优化的核心音频分析类""" def __init__(self, models): self.keywords = { "opening": ["您好", "请问是", "先生/女士", "很高兴为您服务"], "closing": ["感谢接听", "祝您生活愉快", "再见", "有问题随时联系"], "forbidden": ["不可能", "没办法", "我不管", "随便你", "投诉也没用"], "solution": ["解决", "处理好了", "已完成", "满意吗", "还有问题吗"] } self.synonyms = { "不可能": ["不可能", "没可能", "做不到", "无法做到"], "解决": ["极狐", "处理", "完成", "搞定", "办妥"] } self.models = models self.models_loaded = True if models else False def load_keywords(self, excel_path): """从Excel加载关键词和同义词""" try: # 使用更健壮的Excel读取方式 df = pd.read_excel(excel_path, sheet_name=None) if "开场白" in df: self.keywords["opening"] = df["开场白"].dropna()["关键词"].tolist() if "结束语" in df: self.keywords["closing"] = df["结束语"].dropna()["关键词"].tolist() if "禁语" in df: self.keywords["forbidden"] = df["禁语"].dropna()["关键词"].tolist() if "解决关键词" in极狐 df: self.keywords["solution"] = df["解决关键词"].dropna()["关键词"].tolist() # 加载同义词表 if "同义词" in df: for _, row in df["同义词"].iterrows(): main_word = row["主词"] synonyms = row["同义词"].split("、") self.synonyms[main_word] = synonyms return True, "关键词加载成功" except Exception as e: error_msg = f"加载关键词失败: {str(e)}" return False, error_msg def convert_audio(self, input_path): """转换音频为WAV格式并分块,使用临时目录管理""" # 添加音频文件校验 if not os.path.exists(input_path): raise Exception(f"文件不存在: {input_path}") if os.path.getsize(input_path) == 0: raise Exception("文件为空") valid_extensions = ['.mp3', '.wav', '.amr', '.flac', '.m4a', '.ogg'] _, ext = os.path.splitext(input_path) if ext.lower() not in valid_extensions: raise Exception(f"不支持的文件格式: {ext}") temp_dir = None # 初始化为None try: # 检查原始音频格式,符合条件则跳过转换 _, ext = os.path.splitext(input_path) if ext.lower() in ['.wav', '.wave']: # 检查文件格式是否符合要求 audio = AudioSegment.from_file(input_path) if (audio.frame_rate == MODEL_CONFIG["sample_rate"] and audio.channels == 1 and audio.sample_width == 2): # 16位PCM # 符合要求的WAV文件,直接使用 chunks = [] chunk_size = MODEL_CONFIG["chunk_size"] * 1000 # 毫秒 # 创建临时目录用于分块 temp_dir = tempfile.TemporaryDirectory() for i in range(0, len(audio), chunk_size): chunk = audio[i:i + chunk_size] chunk_path = os.path.join(temp_dir.name, f"chunk_{i // chunk_size}.wav") chunk.export(chunk_path, format="wav") chunks.append({ "path": chunk_path, "start_time": i / 1000.0, "end_time": (i + len(chunk)) / 1000.0 }) return chunks, len(audio) / 1000.0, temp_dir # 创建临时目录 temp_dir = tempfile.TemporaryDirectory() # 读取音频文件 audio = AudioSegment.from_file(input_path) # 转换为单声道16kHz audio = audio.set_frame_rate(MODEL_CONFIG["sample_rate"]) audio = audio.set_channels(1) # 计算总时长 duration = len(audio) / 1000.0 # 毫秒转秒 # 分块处理(10秒) chunks = [] chunk_size = MODEL_CONFIG["chunk_size"] * 1000 # 毫秒 for i in range(0, len(audio), chunk_size): chunk = audio[i:i + chunk_size] chunk_path = os.path.join(temp_dir.name, f"chunk_{i // chunk_size}.wav") chunk.export(chunk_path, format="wav") chunks.append({ "path": chunk_path, "start_time": i / 1000.0, # 全局起始时间(秒) "end_time": (i + len(chunk)) / 1000.0 # 全局结束时间(秒) }) return chunks, duration, temp_dir except Exception as e: error_msg = f"音频转换失败: {str(e)}" # 安全清理临时目录 if temp_dir: try: temp_dir.cleanup() except: pass return [], 0, None def diarize_speakers(self, audio_path): """说话人分离 - 合并连续片段""" try: diarization = self.models["diarization_pipeline"](audio_path) segments = [] current_segment = None # 合并连续相同说话人的片段 for turn, _, speaker in diarization.itertracks(yield_label=True): if current_segment is None: # 第一个片段 current_segment = { "start": turn.start, "end": turn.end, "speaker": speaker } elif current_segment["speaker"] == speaker and (turn.start - current_segment["end"]) < 1.0: # 相同说话人且间隔小于1秒,合并片段 current_segment["end"] = turn.end else: # 不同说话人或间隔过大,保存当前片段并开始新片段 segments.append(current_segment) current_segment = { "start": turn.start, "end": turn.end, "speaker": speaker } # 添加最后一个片段 if current_segment: segments.append(current_segment) # 添加文本占位符 for segment in segments: segment["text"] = "" return segments except Exception as e: error_msg = f"说话人分离失败: {str(e)}" raise Exception(error_msg) from e def transcribe_audio_batch(self, chunk_paths): """批量语音识别多个分块""" try: # 批量处理音频分块 results = self.models["asr_pipeline"]( chunk_paths, chunk_length_s=MODEL_CONFIG["chunk_size"], stride_length_s=(4, 2), batch_size=MODEL_CONFIG["batch_size"], return_timestamps=True ) # 整理结果 transcribed_data = [] for result in results: text = result["text"] chunks = result["chunks"] transcribed_data.append((text, chunks)) return transcribed_data except Exception as e: error_msg = f"语音识别失败: {str(e)}" raise Exception(error_msg) from e def analyze_sentiment_batch(self, texts, context_weights=None): """批量情感分析 - 支持长文本处理和上下文权重""" try: if not texts: return [] # 应用上下文权重(如果有) if context_weights is None: context_weights = [1.0] * len(texts) # 预处理文本 - 截断并添加特殊token inputs = self.models["sentiment_tokenizer"]( texts, padding=True, truncation=True, max_length=512, return_tensors="pt" ).to(MODEL_CONFIG["device"]) # 批量推理 with torch.no_grad(): outputs = self.models["sentiment_model"](**inputs) # 计算概率 probs = torch.softmax(outputs.logits, dim=-1).cpu().numpy() # 处理结果 results = [] labels = ["积极", "消极", "中性"] for i, text in enumerate(texts): base_probs = probs[i] weight = context_weights[i] # 应用上下文权重 weighted_probs = base_probs * weight sentiment = labels[np.argmax(weighted_probs)] # 情感强度检测 strong_negative = weighted_probs[1] > 0.7 # 消极概率超过70% strong_positive = weighted_probs[0] > 0.7 # 积极概率超过70% # 特定情绪检测 specific_emotion = "无" if "生气" in text or "愤怒" in text or "气死" in text: specific_emotion = "愤怒" elif "不耐烦" in text or "快点" in text or "急死" in text: specific_emotion = "不耐烦" elif "失望" in text or "无奈" in text: specific_emotion = "失望" # 如果有强烈情感则覆盖平均结果 if strong_negative: sentiment = "强烈消极" elif strong_positive: sentiment = "强烈积极" results.append({ "sentiment": sentiment, "emotion": specific_emotion, "s极狐": weighted_probs.tolist(), "weight": weight }) return results except Exception as e: error_msg = f"情感分析失败: {str(e)}" raise Exception(error_msg) from e def match_keywords(self, text, keyword_type): """高级关键词匹配 - 使用分词和同义词""" # 获取关键词列表 keywords = self.keywords.get(keyword_type, []) if not keywords: return False # 分词处理 words = jieba.lcut(text) # 检查每个关键词 for keyword in keywords: # 检查直接匹配 if keyword in text: return True # 检查同义词 synonyms = self.synonyms.get(keyword, []) for synonym in synonyms: if synonym in text: return True # 检查分词匹配(全词匹配) if keyword in words: return True return False def identify_agent(self, segments, full_text): """智能客服身份识别""" # 候选客服信息 candidates = {} # 特征1:开场白关键词 for i, segment in enumerate(segments[:5]): # 检查前5个片段 if self.match_keywords(segment["text"], "opening"): speaker = segment["speaker"] candidates.setdefault(speaker, {"score": 0, "segments": []}) candidates[speaker]["score"] += 3 # 开场白权重高 candidates[speaker]["segments"].append(i) # 特征2:结束语关键词 for i, segment in enumerate(segments[-3:]): # 检查最后3个片段 if self.match_keywords(segment["text"], "closing"): speaker = segment["speaker"] candidates.setdefault(speaker, {"score": 0, "segments": []}) candidates[speaker]["score"] += 2 # 结束语权重中等 candidates[speaker]["segments"].append(len(segments) - 3 + i) # 特征3:说话时长 speaker_durations = {} for segment in segments: duration = segment["end"] - segment["start"] speaker_durations[segment["speaker"]] = speaker_durations.get(segment["speaker"], 0) + duration # 为说话时长最长的加分 if speaker_durations: max_duration = max(speaker_durations.values()) for speaker, duration in speaker_durations.items(): candidates.setdefault(speaker, {"score": 0, "segments": []}) if duration == max_duration: candidates[speaker]["score"] += 1 # 特征4:客服特定词汇出现频率 agent_keywords = ["客服", "代表", "专员", "先生", "女士"] speaker_keyword_count = {} for segment in segments: text = segment["text"] speaker = segment["speaker"] for word in agent_keywords: if word in text: speaker_keyword_count[speaker] = speaker_keyword_count.get(speaker, 0) + 1 # 为关键词出现最多的加分 if speaker_keyword_count: max_count = max(speaker_keyword_count.values()) for speaker, count in speaker_keyword_count.items(): if count == max_count: candidates.setdefault(speaker, {"score": 0, "segments": []}) candidates[speaker]["score"] += 1 # 选择得分最高的作为客服 if candidates: best_speaker = max(candidates.items(), key=lambda x: x[1]["score"])[0] return best_speaker # 默认选择第一个说话人 return segments[0]["speaker"] if segments else None def associate_speaker_text(self, segments, full_text_chunks): """基于时间重叠度的说话人-文本关联""" for segment in segments: segment_text = "" segment_start = segment["start"] segment_end = segment["end"] for word_info in full_text_chunks: if "global_start" not in word_info: continue word_start = word_info["global_start"] word_end = word_info["global_end"] # 计算重叠度 overlap_start = max(segment_start, word_start) overlap_end = min(segment_end, word_end) overlap = max(0, overlap_end - overlap_start) # 计算重叠比例 word_duration = word_end - word_start segment_duration = segment_end - segment_start if overlap > 0: # 如果重叠超过50%或单词完全在片段内 if (overlap / word_duration > 0.5) or (overlap / segment_duration > 0.5): segment_text += word_info["text"] + " " segment["text"] = segment_text.strip() def analyze_audio(self, audio_path): """完整分析单个音频文件 - 优化版本""" try: # 步骤1: 转换音频并分块(使用临时目录) chunks, duration, temp_dir = self.convert_audio(audio_path) if not chunks or not temp_dir: raise Exception("音频转换失败或未生成分块") try: # 步骤2: 说话人分离 segments = self.diarize_speakers(audio_path) # 步骤3: 批量语音识别 chunk_paths = [chunk["path"] for chunk in chunks] transcribed_data = self.transcribe_audio_batch(chunk_paths) # 步骤4: 处理识别结果 full_text_chunks = [] for idx, (text, chunk_data) in enumerate(transcribed_data): chunk = chunks[idx] # 调整时间戳为全局时间 for word_info in chunk_data: if "timestamp" in word_info: start, end = word_info["timestamp"] word_info["global_start"] = chunk["start_time"] + start word_info["global_end"] = chunk["start_time"] + end else: word_info["global_start"] = chunk["start_time"] word_info["global_end"] = chunk["end_time"] full_text_chunks.extend(chunk_data) # 步骤5: 基于时间重叠度关联说话人和文本 self.associate_speaker_text(segments, full_text_chunks) # 步骤6: 智能识别客服身份 agent_id = self.identify_agent(segments, full_text_chunks) # 步骤7: 提取客服和客户文本 agent_text = "" customer_text = "" opening_found = False closing_found = False forbidden_found = False agent_weights = [] # 单独收集客服权重 customer_weights = [] # 单独收集客户权重 negative_context = False # 用于情感分析上下文权重 # 收集上下文信息用于情感权重 for i, segment in enumerate(segments): if segment["speaker"] == agent_id: agent_text += segment["text"] + " " agent_weights.append(1.2 if negative_context else 1.0) # 客服在消极上下文后权重更高 else: customer_text += segment["text"] + " " customer_weights.append(1.0) # 客户权重不变 # 检测消极情绪上下文 if "生气" in segment["text"] or "愤怒" in segment["text"] or "失望" in segment["text"]: negative_context = True elif "解决" in segment["text"] or "满意" in segment["text"]: negative_context = False # 使用高级关键词匹配 if not opening_found and self.match_keywords(segment["text"], "opening"): opening_found = True if not closing_found and self.match_keywords(segment["text"], "closing"): closing_found = True if not forbidden_found and self.match_keywords(segment["text"], "forbidden"): forbidden_found = True # 步骤8: 批量情感分析 - 应用平均权重 agent_avg_weight = np.mean(agent_weights) if agent_weights else 1.0 customer_avg_weight = np.mean(customer_weights) if customer_weights else 1.0 sentiment_results = self.analyze_sentiment_batch( [agent_text, customer_text], context_weights=[agent_avg_weight, customer_avg_weight] ) if sentiment_results: agent_sentiment = sentiment_results[0]["sentiment"] agent_emotion = sentiment_results[0]["emotion"] customer_sentiment = sentiment_results[1]["sentiment"] customer_emotion = sentiment_results[1]["emotion"] else: agent_sentiment = "未知" agent_emotion = "无" customer_sentiment = "未知" customer_emotion = "无" # 问题解决率分析 solution_found = self.match_keywords(agent_text, "solution") # 语速分析 agent_words = len(agent_text.split()) agent_duration = sum([s["end"] - s["start"] for s in segments if s["speaker"] == agent_id]) agent_speed = agent_words / (agent_duration / 60) if agent_duration > 0 else 0 # 词/分钟 # 音量分析(简单版) try: y, sr = librosa.load(audio_path, sr=MODEL_CONFIG["sample_rate"]) rms = librosa.feature.rms(y=y) avg_volume = np.mean(rms) volume_stability = np.std(rms) / avg_volume if avg_volume > 0 else 0 except: avg_volume = 0 volume_stability = 0 # 构建结果 result = { "file_name": os.path.basename(audio_path), "duration": round(duration, 2), "opening_check": "是" if opening_found else "否", "closing_check": "是" if closing_found else "极狐", "forbidden_check": "是" if forbidden_found else "否", "agent_sentiment": agent_sentiment, "agent_emotion": agent_emotion, "customer_sentiment": customer_sentiment, "customer_emotion": customer_emotion, "agent_speed": round(agent_speed, 1), "volume_level": round(avg_volume, 4), "volume_stability": round(volume_stability, 2), "solution_rate": "是" if solution_found else "否", "agent_text": agent_text[:500] + "..." if len(agent_text) > 500 else agent_text, "customer_text": customer_text[:500] + "..." if len(customer_text) > 500 else customer_text } return result finally: # 自动清理临时目录 try: temp_dir.cleanup() except Exception as e: print(f"清理临时目录失败: {str(e)}") except Exception as e: error_msg = f"分析文件 {os.path.basename(audio_path)} 时出错: {str(e)}" raise Exception(error_msg) from e class AnalysisThread(QThread): """分析线程 - 并行优化版本""" progress = pyqtSignal(int, str) result_ready = pyqtSignal(dict) finished_all = pyqtSignal() error_occurred = pyqtSignal(str, str) def __init__(self, audio_files, keywords_file, output_dir, models): super().__init__() self.audio_files = audio_files self.keywords_file = keywords_file self.output_dir = output_dir self.stop_requested = False self.analyzer = AudioAnalyzer(models) self.completed_count = 0 self.executor = None # 用于线程池引用 def run(self): try: total = len(self.audio_files) # 加载关键词 if self.keywords_file: success, msg = self.analyzer.load_keywords(self.keywords_file) if not success: self.error_occurred.emit("关键词加载", msg) results = [] errors = [] # 使用线程池进行并行处理 with ThreadPoolExecutor(max_workers=MODEL_CONFIG["max_workers"]) as executor: self.executor = executor # 保存引用用于停止操作 # 提交所有任务 future_to_file = { executor.submit(self.analyzer.analyze_audio, audio_file): audio_file for audio_file in self.audio_files } # 处理完成的任务 for future in as_completed(future_to_file): if self.stop_requested: break audio_file = future_to_file[future] try: result = future.result() if result: results.append(result) self.result_ready.emit(result) except Exception as e: error_msg = str(e) errors.append({ "file": audio_file, "error": error_msg }) self.error_occurred.emit(os.path.basename(audio_file), error_msg) # 更新进度 self.completed_count += 1 progress = int(self.completed_count / total * 100) self.progress.emit( progress, f"已完成 {self.completed_count}/{total} ({progress}%)" ) # 生成报告 if results: self.generate_reports(results, errors) self.finished_all.emit() except Exception as e: self.error_occurred.emit("全局错误", str(e)) def stop(self): """停止分析 - 强制终止线程池任务""" self.stop_requested = True # 强制终止线程池中的任务 if self.executor: # 先尝试优雅关闭 self.executor.shutdown(wait=False) # 强制取消所有未完成的任务 for future in self.executor._futures: if not future.done(): future.cancel() def generate_reports(self, results, errors): """生成Excel和Word报告 - 优化版本""" try: # 生成Excel报告 df = pd.DataFrame(results) excel_path = os.path.join(self.output_dir, "质检分析报告.xlsx") # 创建Excel写入器 with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer: df.to_excel(writer, sheet_name='详细结果', index=False) # 添加统计摘要 stats_data = { "指标": ["分析文件总数", "成功分析文件数", "分析失败文件数", "开场白合格率", "结束语合格率", "禁语出现率", "客服积极情绪占比", "客户消极情绪占比", "问题解决率"], "数值": [ len(results) + len(errors), len(results), len(errors), f"{df['opening_check'].value_counts(normalize=True).get('是', 0) * 100:.1f}%", f"{df['closing_check'].value_counts(normalize=True).get('是', 0) * 100:.1f}%", f"{df['forbidden_check'].value_counts(normalize=True).get('是', 0) * 100:.1极狐%", f"{df[df['agent_sentiment'] == '积极'].shape[0] / len(df) * 100:.1f}%", f"{df[df['customer_sentiment'] == '消极'].shape[0] / len(df) * 100:.1f}%", f"{df['solution_rate'].value_counts(normalize=True).get('是', 0) * 100:.1f}%" ] } stats_df = pd.DataFrame(stats_data) stats_df.to_excel(writer, sheet_name='统计摘要', index=False) # 生成Word报告 doc = Document() doc.add_heading('外呼电话质检分析汇总报告', 0) # 添加统计信息 doc.add_heading('整体统计', level=1) stats = [ f"分析文件总数: {len(results) + len(errors)}", f"成功分析文件数: {len(results)}", f"分析失败文件数: {len(errors)}", f"开场白合格率: {stats_data['数值'][3]}", f"结束语合格率: {stats_data['数值'][4]}", f"禁语出现率: {stats_data['数值'][5]}", f"客服积极情绪占比: {stats_data['数值'][6]}", f"客户消极情绪占比: {stats_data['数值'][7]}", f"问题解决率: {stats_data['数值'][8]}" ] for stat in stats: doc.add_paragraph(stat) # 添加图表 self.add_charts(doc, df) # 添加错误列表 if errors: doc.add_heading('分析失败文件', level=1) table = doc.add_table(rows=1, cols=2) hdr_cells = table.rows[0].cells hdr_cells[0].text = '文件' hdr_cells[1].text = '错误原因' for error in errors: row_cells = table.add_row().cells row_cells[0].text = os.path.basename(error['file']) row_cells[1].text = error['error'] word_path = os.path.join(self.output_dir, "可视化分析报告.docx") doc.save(word_path) return True, f"报告已保存到: {self.output_dir}" except Exception as e: return False, f"生成报告失败: {str(e)}" def add_charts(self, doc, df): """在Word文档中添加图表 - 显式释放内存""" try: # 客服情感分布 fig1, ax1 = plt.subplots(figsize=(6, 4)) sentiment_counts = df['agent_sentiment'].value_counts() sentiment_counts.plot(kind='bar', ax=ax1, color=['green', 'red', 'blue', 'darkred', 'darkgreen']) ax1.set_title('客服情感分布') ax1.set_xlabel('情感类型') ax1.set_ylabel('数量') fig1.tight_layout() fig1.savefig('agent_sentiment.png') doc.add_picture('agent_sentiment.png', width=Inches(5)) os.remove('agent_sentiment.png') plt.close(fig1) # 显式关闭图表释放内存 # 客户情感分布 fig2, ax2 = plt.subplots(figsize=(6, 4)) df['customer_sentiment'].value_counts().plot(kind='bar', ax=ax2, color=['green', 'red', 'blue', 'darkred', 'darkgreen']) ax2.set_title('客户情感分布') ax2.set_xlabel('情感类型') ax2.set_ylabel('数量') fig2.tight_layout() fig2.savefig('customer_sentiment.png') doc.add_picture('customer_sentiment.png', width=Inches(5)) os.remove('customer_sentiment.png') plt.close(fig2) # 显式关闭图表释放内存 # 合规性检查 fig3, ax3 = plt.subplots(figsize=(6, 4)) compliance = df[['opening_check', 'closing_check', 'forbidden_check']].apply( lambda x: x.value_counts().get('是', 0)) compliance.plot(kind='bar', ax=ax3, color=['blue', 'green', 'red']) ax3.set_title('合规性检查') ax3.set_xlabel('检查项') ax3.set_ylabel('合格数量') fig3.tight_layout() fig3.savefig('compliance.png') doc.add_picture('compliance.png', width=Inches(5)) os.remove('compliance.png') plt.close(fig3) # 显式关闭图表释放内存 except Exception as e: print(f"生成图表失败: {str(e)}") # 确保异常情况下也关闭所有图表 if 'fig1' in locals(): plt.close(fig1) if 'fig2' in locals(): plt.close(fig2) if 'fig3' in locals(): plt.close(fig3) class MainWindow(QMainWindow): """主界面 - 优化版本""" def __init__(self): super().__init__() self.setWindowTitle("外呼电话录音质检分析系统") self.setGeometry(100, 100, 1000, 800) # 初始化变量 self.audio_files = [] self.keywords_file = "" self.output_dir = os.getcwd() self.analysis_thread = None self.model_loader = None self.models = {} self.models_loaded = False # 初始化为False # 设置全局字体 app_font = QFont("Microsoft YaHei", 10) QApplication.setFont(app_font) # 创建主布局 main_widget = QWidget() main_layout = QVBoxLayout() main_layout.setSpacing(10) main_layout.setContentsMargins(15, 15, 15, 15) # 状态栏 self.status_label = QLabel("准备就绪") self.status_label.setAlignment(Qt.AlignCenter) self.status_label.setStyleSheet("background-color: #f0f0f0; padding: 5px; border-radius: 5px;") # 文件选择区域 file_group = QGroupBox("文件选择") file_layout = QVBoxLayout() file_layout.setSpacing(10) # 音频选择 audio_layout = QHBoxLayout() self.audio_label = QLabel("音频文件/文件夹:") self.audio_path_edit = QLineEdit() self.audio极狐_edit.setReadOnly(True) self.audio_path_edit.setPlaceholderText("请选择音频文件或文件夹") self.audio_browse_btn = QPushButton("浏览...") self.audio_browse_btn.setFixedWidth(80) self.audio_browse_btn.clicked.connect(self.browse_audio) audio_layout.addWidget(self.audio_label) audio_layout.addWidget(self.audio_path_edit, 1) audio_layout.addWidget(self.audio_browse_btn) # 关键词选择 keyword_layout = QHBoxLayout() self.keyword_label = QLabel("关键词文件:") self.keyword_path_edit = QLineEdit() self.keyword_path_edit.setReadOnly(True) self.keyword_path_edit.setPlaceholderText("可选:选择关键词Excel文件") self.keyword_browse_btn = QPushButton("浏览...") self.keyword_browse_btn.setFixedWidth(80) self.keyword_browse_btn.clicked.connect(self.browse_keywords) keyword_layout.addWidget(self.keyword_label) keyword_layout.addWidget(self.keyword_path_edit, 1) keyword_layout.addWidget(self.keyword_browse_btn) # 输出目录 output_layout = QHBoxLayout() self.output_label = QLabel("输出目录:") self.output_path_edit = QLineEdit(os.getcwd()) self.output_path_edit.setReadOnly(True) self.output_browse_btn = QPushButton("浏览...") self.output_browse_btn.setFixedWidth(80) self.output_browse_btn.clicked.connect(self.browse_output) output_layout.addWidget(self.output_label) output_layout.addWidget(self.output_path_edit, 1) output_layout.addWidget(self.output_browse_btn) file_layout.addLayout(audio_layout) file_layout.addLayout(keyword_layout) file_layout.addLayout(output_layout) file_group.setLayout(file_layout) # 控制按钮区域 control_layout = QHBoxLayout() control_layout.setSpacing(15) self.start_btn = QPushButton("开始分析") self.start_btn.setFixedHeight(40) self.start_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") self.start_btn.clicked.connect(self.start_analysis) self.stop_btn = QPushButton("停止分析") self.stop_btn.setFixedHeight(40) self.stop_btn.setStyleSheet("background-color: #f44336; color: white; font-weight: bold;") self.stop_btn.clicked.connect(self.stop_analysis) self.stop_btn.setEnabled(False) self.clear_btn = QPushButton("清空") self.clear_btn.setFixedHeight(40) self.clear_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") self.clear_btn.clicked.connect(self.clear_all) # 添加模型重试按钮 self.retry_btn = QPushButton("重试加载模型") self.retry_btn.setFixedHeight(40) self.retry_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") self.retry_btn.clicked.connect(self.retry_load_models) self.retry_btn.setVisible(False) # 初始隐藏 control_layout.addWidget(self.start_btn) control_layout.addWidget(self.stop_btn) control_layout.addWidget(self.clear_btn) control_layout.addWidget(self.retry_btn) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setTextVisible(True) self.progress_bar.setStyleSheet("QProgressBar {border: 1px solid grey; border-radius: 5px; text-align: center;}" "QProgressBar::chunk {background-color: #4CAF50; width: 10px;}") # 结果展示区域 result_group = QGroupBox("分析结果") result_layout = QVBoxLayout() result_layout.setSpacing(10) # 结果标签 result_header = QHBoxLayout() self.result_label = QLabel("分析结果:") self.result_count_label = QLabel("0/0") self.result_count_label.setAlignment(Qt.AlignRight) result_header.addWidget(self.result_label) result_header.addWidget(self.result_count_label) self.result_text = QTextEdit() self.result_text.setReadOnly(True) self.result_text.setStyleSheet("font-family: Consolas, 'Microsoft YaHei';") # 错误列表 error_header = QHBoxLayout() self.error_label = QLabel("错误信息:") self.error_count_label = QLabel("0") self.error_count_label.setAlignment(Qt.AlignRight) error_header.addWidget(self.error_label) error_header.addWidget(self.error_count_label) self.error_list = QListWidget() self.error_list.setFixedHeight(120) self.error_list.setStyleSheet("color: #d32f2f;") result_layout.addLayout(result_header) result_layout.addWidget(self.result_text) result_layout.addLayout(error_header) result_layout.addWidget(self.error_list) result_group.setLayout(result_layout) # 添加到主布局 main_layout.addWidget(file_group) main_layout.addLayout(control_layout) main_layout.addWidget(self.progress_bar) main_layout.addWidget(self.status_label) main_layout.addWidget(result_group) main_widget.setLayout(main_layout) self.setCentralWidget(main_widget) # 启动模型加载 self.load_models() def load_models(self): """后台加载模型""" self.status_label.setText("正在加载AI模型,请稍候...") self.start_btn.setEnabled(False) self.retry_btn.setVisible(False) # 隐藏重试按钮 self.model_loader = ModelLoader() self.model_loader.progress.connect(self.update_model_loading_status) self.model_loader.finished.connect(self.handle_model_loading_finished) self.model_loader.start() def retry_load_models(self): """重试加载模型""" self.retry_btn.setVisible(False) self.load_models() def update_model_loading_status(self, message): """更新模型加载状态""" self.status_label.setText(message) def handle_model_loading_finished(self, success, message): """处理模型加载完成""" if success: self.models = self.model_loader.models self.models_loaded = True self.status_label.setText(message) self.start_btn.setEnabled(True) self.retry_btn.setVisible(False) else: self.status_label.setText(message) self.start_btn.setEnabled(False) self.retry_btn.setVisible(True) # 显示重试按钮 QMessageBox.critical(self, "模型加载失败", f"{message}\n\n点击'重试加载模型'按钮尝试重新加载") def browse_audio(self): """选择音频文件或文件夹""" options = QFileDialog.Options() files, _ = QFileDialog.getOpenFileNames( self, "选择音频文件", "", "音频文件 (*.mp3 *.wav *.amr *.flac *.m4a);;所有文件 (*)", options=options ) if files: self.audio_files = files self.audio_path_edit.setText(f"已选择 {len(files)} 个文件") self.result_count_label.setText(f"0/{len(files)}") def browse_keywords(self): """选择关键词文件""" options = QFileDialog.Options() file, _ = QFileDialog.getOpenFileName( self, "选择关键词文件", "", "Excel文件 (*.xlsx);;所有文件 (*)", options=options ) if file: self.keywords_file = file self.keyword_path_edit.setText(os.path.basename(file)) def browse_output(self): """选择输出目录""" options = QFileDialog.Options() directory = QFileDialog.getExistingDirectory( self, "选择输出目录", options=options ) if directory: self.output_dir = directory self.output_path_edit.setText(directory) def start_analysis(self): """开始分析""" if not self.audio_files: self.show_message("错误", "请先选择音频文件!") return if not self.models_loaded: self.show_message("错误", "AI模型尚未加载完成!") return # 检查输出目录 if not os.path.exists(self.output_dir): try: os.makedirs(self.output_dir) except Exception as e: self.show_message("错误", f"无法创建输出目录: {str(e)}") return # 更新UI状态 self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.result_text.clear() self.error_list.clear() self.error_count_label.setText("0") self.result_text.append("开始分析音频文件...") self.progress_bar.setValue(0) # 创建并启动分析线程 self.analysis_thread = AnalysisThread( self.audio_files, self.keywords_file, self.output_dir, self.models ) # 连接信号 self.analysis_thread.progress.connect(self.update_progress) self.analysis_thread.result_ready.connect(self.handle_result) self.analysis_thread.finished_all.connect(self.analysis_finished) self.analysis_thread.error_occurred.connect(self.handle_error) self.analysis_thread.start() def stop_analysis(self): """停止分析""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.wait() self.result_text.append("分析已停止") self.status_label.setText("分析已停止") self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) def clear_all(self): """清空所有内容""" self.audio_files = [] self.keywords_file = "" self.audio_path_edit.clear() self.keyword_path_edit.clear() self.result_text.clear() self.error_list.clear() self.progress_bar.setValue(0) self.status_label.setText("准备就绪") self.result_count_label.setText("0/0") self.error_count_label.setText("0") def update_progress(self, value, message): """更新进度""" self.progress_bar.setValue(value) self.status_label.setText(message) # 更新结果计数 if "已完成" in message: parts = message.split() if len(parts) >= 2: self.result_count_label.setText(parts[1]) def handle_result(self, result): """处理单个结果""" summary = f""" 文件: {result['file_name']} 时长: {result['duration']}秒 ---------------------------------------- 开场白: {result['opening_check']} | 结束语: {result['closing_check']} | 禁语: {result['forbidden_check']} 客服情感: {result['agent_sentiment']} ({result['agent_emotion']}) | 语速: {result['agent_speed']}词/分 客户情感: {result['customer_sentiment']} ({result['customer_emotion']}) 问题解决: {result['solution_rate']} 音量水平: {result['volume_level']} | 稳定性: {result['volume_stability']} ---------------------------------------- """ self.result_text.append(summary) def handle_error(self, file_name, error): """处理错误""" self.error_list.addItem(f"{file_name}: {error}") self.error_count_label.setText(str(self.error_list.count())) def analysis_finished(self): """分析完成""" self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) self.status_label.setText(f"分析完成! 报告已保存到: {self.output_dir}") self.result_text.append("分析完成!") # 显示完成消息 self.show_message("完成", f"分析完成! 报告已保存到: {self.output_dir}") def show_message(self, title, message): """显示消息对话框""" msg = QMessageBox(self) msg.setWindowTitle(title) msg.setText(message) msg.setStandardButtons(QMessageBox.Ok) msg.exec_() if __name__ == "__main__": app = QApplication(sys.argv) # 检查GPU可用性 if MODEL_CONFIG["device"] == "cuda": try: gpu_mem = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3) print(f"GPU内存: {gpu_mem:.2f}GB") # 根据GPU内存调整并行度 if gpu_mem < 4: # 确保有足够内存 MODEL_CONFIG["device"] = "cpu" MODEL_CONFIG["max_workers"] = 4 print("GPU内存不足,切换到CPU模式") elif gpu_mem < 8: MODEL_CONFIG["max_workers"] = 2 else: MODEL_CONFIG["max_workers"] = 4 except: MODEL_CONFIG["device"] = "cpu" MODEL_CONFIG["max_workers"] = 4 print("无法获取GPU信息,切换到CPU模式") window = MainWindow() window.show() sys.exit(app.exec_())

检查代码是否可运行,是否高效,是否可CPUimport sys import os import json import time import wave import numpy as np import pandas as pd import matplotlib.pyplot as plt import soundfile as sf from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QTextEdit, QFileDialog, QProgressBar, QGroupBox, QComboBox, QCheckBox, QMessageBox) from PyQt5.QtCore import QThread, pyqtSignal from pydub import AudioSegment from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification import whisper from pyannote.audio import Pipeline from docx import Document from docx.shared import Inches import librosa import tempfile from collections import defaultdict import re from concurrent.futures import ThreadPoolExecutor, as_completed import torch from torch.cuda import is_available as cuda_available import logging import gc # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # 全局模型缓存 MODEL_CACHE = {} class AnalysisThread(QThread): progress = pyqtSignal(int) message = pyqtSignal(str) analysis_complete = pyqtSignal(dict) error = pyqtSignal(str) def __init__(self, audio_files, keyword_file, whisper_model_path, pyannote_model_path, emotion_model_path): super().__init__() self.audio_files = audio_files self.keyword_file = keyword_file self.whisper_model_path = whisper_model_path self.pyannote_model_path = pyannote_model_path self.emotion_model_path = emotion_model_path self.running = True self.cached_models = {} self.temp_files = [] # 用于管理临时文件 self.lock = torch.multiprocessing.Lock() # 用于模型加载的锁 def run(self): try: # 加载关键词 self.message.emit("正在加载关键词...") keywords = self.load_keywords() # 预加载模型 self.message.emit("正在预加载模型...") self.preload_models() results = [] total_files = len(self.audio_files) for idx, audio_file in enumerate(self.audio_files): if not self.running: self.message.emit("分析已停止") return self.message.emit(f"正在处理文件: {os.path.basename(audio_file)} ({idx + 1}/{total_files})") file_result = self.analyze_file(audio_file, keywords) if file_result: results.append(file_result) # 定期清理内存 if idx % 5 == 0: gc.collect() torch.cuda.empty_cache() if cuda_available() else None self.progress.emit(int((idx + 1) / total_files * 100)) self.analysis_complete.emit({"results": results, "keywords": keywords}) self.message.emit("分析完成!") except Exception as e: import traceback error_msg = f"分析过程中发生错误: {str(e)}\n{traceback.format_exc()}" self.error.emit(error_msg) logger.error(error_msg) finally: # 清理临时文件 self.cleanup_temp_files() def cleanup_temp_files(self): """清理所有临时文件""" for temp_file in self.temp_files: if os.path.exists(temp_file): try: os.unlink(temp_file) except Exception as e: logger.warning(f"删除临时文件失败: {temp_file}, 原因: {str(e)}") def preload_models(self): """预加载所有模型到缓存(添加线程安全)""" global MODEL_CACHE # 使用锁确保线程安全 with self.lock: # 检查全局缓存是否已加载模型 if 'whisper' in MODEL_CACHE and 'pyannote' in MODEL_CACHE and 'emotion_classifier' in MODEL_CACHE: self.cached_models = MODEL_CACHE self.message.emit("使用缓存的模型") return self.cached_models = {} try: # 加载语音识别模型 if 'whisper' not in MODEL_CACHE: self.message.emit("正在加载语音识别模型...") MODEL_CACHE['whisper'] = whisper.load_model( self.whisper_model_path, device="cuda" if cuda_available() else "cpu" ) self.cached_models['whisper'] = MODEL_CACHE['whisper'] # 加载说话人分离模型 if 'pyannote' not in MODEL_CACHE: self.message.emit("正在加载说话人分离模型...") MODEL_CACHE['pyannote'] = Pipeline.from_pretrained( self.pyannote_model_path, use_auth_token=True ) self.cached_models['pyannote'] = MODEL_CACHE['pyannote'] # 加载情感分析模型 if 'emotion_classifier' not in MODEL_CACHE: self.message.emit("正在加载情感分析模型...") device = 0 if cuda_available() else -1 tokenizer = AutoTokenizer.from_pretrained(self.emotion_model_path) model = AutoModelForSequenceClassification.from_pretrained(self.emotion_model_path) # 尝试使用半精度浮点数减少内存占用 try: if device != -1: model = model.half() except Exception: pass # 如果失败则继续使用全精度 MODEL_CACHE['emotion_classifier'] = pipeline( "text-classification", model=model, tokenizer=tokenizer, device=device ) self.cached_models['emotion_classifier'] = MODEL_CACHE['emotion_classifier'] except Exception as e: raise Exception(f"模型加载失败: {str(e)}") def analyze_file(self, audio_file, keywords): """分析单个音频文件(优化内存使用)""" try: # 确保音频为WAV格式 wav_file, is_temp = self.convert_to_wav(audio_file) if is_temp: self.temp_files.append(wav_file) # 获取音频信息 duration, sample_rate, channels = self.get_audio_info(wav_file) # 说话人分离 - 使用较小的音频片段处理大文件 diarization = self.process_diarization(wav_file, duration) # 识别客服和客户 agent_segments, customer_segments = self.identify_speakers(wav_file, diarization, keywords['opening']) # 并行处理客服和客户音频 agent_result, customer_result = {}, {} with ThreadPoolExecutor(max_workers=2) as executor: agent_future = executor.submit( self.process_speaker_audio, wav_file, agent_segments, "客服" ) customer_future = executor.submit( self.process_speaker_audio, wav_file, customer_segments, "客户" ) agent_result = agent_future.result() customer_result = customer_future.result() # 情感分析 - 批处理提高效率 agent_emotion, customer_emotion = self.analyze_emotions( [agent_result.get('text', ''), customer_result.get('text', '')] ) # 服务规范检查 opening_check = self.check_opening(agent_result.get('text', ''), keywords['opening']) closing_check = self.check_closing(agent_result.get('text', ''), keywords['closing']) forbidden_check = self.check_forbidden(agent_result.get('text', ''), keywords['forbidden']) # 沟通技巧分析 speech_rate = self.analyze_speech_rate(agent_result.get('segments', [])) volume_analysis = self.analyze_volume(wav_file, agent_segments, sample_rate) # 问题解决率分析 resolution_rate = self.analyze_resolution( agent_result.get('text', ''), customer_result.get('text', ''), keywords['resolution'] ) return { "file_name": os.path.basename(audio_file), "duration": duration, "agent_text": agent_result.get('text', ''), "customer_text": customer_result.get('text', ''), "opening_check": opening_check, "closing_check": closing_check, "forbidden_check": forbidden_check, "agent_emotion": agent_emotion, "customer_emotion": customer_emotion, "speech_rate": speech_rate, "volume_mean": volume_analysis.get('mean', -60), "volume_std": volume_analysis.get('std', 0), "resolution_rate": resolution_rate } except Exception as e: error_msg = f"处理文件 {os.path.basename(audio_file)} 时出错: {str(e)}" self.error.emit(error_msg) logger.error(error_msg, exc_info=True) return None finally: # 清理临时文件 if is_temp and os.path.exists(wav_file): try: os.unlink(wav_file) except Exception: pass def process_diarization(self, wav_file, duration): """分块处理说话人分离,避免大文件内存溢出""" # 对于短音频直接处理 if duration <= 600: # 10分钟以下 return self.cached_models['pyannote'](wav_file) # 对于长音频分块处理 self.message.emit(f"音频较长({duration:.1f}秒),将分块处理...") diarization_result = [] chunk_size = 300 # 5分钟块 for start in range(0, int(duration), chunk_size): if not self.running: return [] end = min(start + chunk_size, duration) self.message.emit(f"处理片段: {start}-{end}秒") # 提取音频片段 with tempfile.NamedTemporaryFile(suffix='.wav') as tmpfile: self.extract_audio_segment(wav_file, start, end, tmpfile.name) segment_diarization = self.cached_models['pyannote'](tmpfile.name) # 调整时间偏移 for segment, _, speaker in segment_diarization.itertracks(yield_label=True): diarization_result.append(( segment.start + start, segment.end + start, speaker )) return diarization_result def extract_audio_segment(self, input_file, start_sec, end_sec, output_file): """提取音频片段""" audio = AudioSegment.from_wav(input_file) start_ms = int(start_sec * 1000) end_ms = int(end_sec * 1000) segment = audio[start_ms:end_ms] segment.export(output_file, format="wav") def process_speaker_audio(self, wav_file, segments, speaker_type): """处理说话人音频(优化内存使用)""" if not segments: return {'text': "", 'segments': []} text = "" segment_details = [] whisper_model = self.cached_models['whisper'] # 处理每个片段 for idx, (start, end) in enumerate(segments): if not self.running: break # 每处理5个片段报告一次进度 if idx % 5 == 0: self.message.emit(f"{speaker_type}: 处理片段 {idx+1}/{len(segments)}") duration = end - start segment_text = self.transcribe_audio_segment(wav_file, start, end, whisper_model) segment_details.append({ 'start': start, 'end': end, 'duration': duration, 'text': segment_text }) text += segment_text + " " return { 'text': text.strip(), 'segments': segment_details } def identify_speakers(self, wav_file, diarization, opening_keywords): """ 改进的客服识别方法 1. 检查前三个片段是否有开场白关键词 2. 如果片段不足三个,则检查所有存在的片段 3. 如果无法确定客服,则默认第二个说话人是客服 """ if not diarization: return [], [] speaker_segments = defaultdict(list) speaker_first_occurrence = {} # 记录每个说话人的首次出现时间 # 收集所有说话人片段并记录首次出现时间 for item in diarization: if len(item) == 3: # 来自分块处理的结果 start, end, speaker = item else: # 来自pyannote的直接结果 segment, _, speaker = item start, end = segment.start, segment.end speaker_segments[speaker].append((start, end)) if speaker not in speaker_first_occurrence or start < speaker_first_occurrence[speaker]: speaker_first_occurrence[speaker] = start # 如果没有说话人 if not speaker_segments: return [], [] # 如果只有一个说话人 if len(speaker_segments) == 1: speaker = list(speaker_segments.keys())[0] return speaker_segments[speaker], [] # 计算每个说话人的开场白得分 speaker_scores = {} whisper_model = self.cached_models['whisper'] for speaker, segments in speaker_segments.items(): score = 0 # 检查前三个片段(如果存在) check_segments = segments[:3] # 最多取前三个片段 for start, end in check_segments: # 转录片段 text = self.transcribe_audio_segment(wav_file, start, end, whisper_model) # 检查开场白关键词 for keyword in opening_keywords: if keyword and keyword in text: score += 1 break # 找到一个关键词就加分并跳出循环 speaker_scores[speaker] = score # 尝试找出得分最高的说话人 max_score = max(speaker_scores.values()) max_speakers = [spk for spk, score in speaker_scores.items() if score == max_score] # 如果有唯一最高分说话人,作为客服 if len(max_speakers) == 1: agent_speaker = max_speakers[0] else: # 无法通过开场白确定客服时,默认第二个说话人是客服 # 按首次出现时间排序 sorted_speakers = sorted(speaker_first_occurrence.items(), key=lambda x: x[1]) # 确保至少有两个说话人 if len(sorted_speakers) >= 2: # 取时间上第二个出现的说话人 agent_speaker = sorted_speakers[1][0] else: # 如果只有一个说话人(理论上不会进入此分支,但安全处理) agent_speaker = sorted_speakers[0][0] # 分离客服和客户片段 agent_segments = speaker_segments[agent_speaker] customer_segments = [] for speaker, segments in speaker_segments.items(): if speaker != agent_speaker: customer_segments.extend(segments) return agent_segments, customer_segments def load_keywords(self): """从Excel文件加载关键词(增强健壮性)""" try: df = pd.read_excel(self.keyword_file) # 确保列存在 columns = ['opening', 'closing', 'forbidden', 'resolution'] for col in columns: if col not in df.columns: raise ValueError(f"关键词文件缺少必要列: {col}") keywords = { "opening": [str(k).strip() for k in df['opening'].dropna().tolist() if str(k).strip()], "closing": [str(k).strip() for k in df['closing'].dropna().tolist() if str(k).strip()], "forbidden": [str(k).strip() for k in df['forbidden'].dropna().tolist() if str(k).strip()], "resolution": [str(k).strip() for k in df['resolution'].dropna().tolist() if str(k).strip()] } # 检查是否有足够的关键词 if not any(keywords.values()): raise ValueError("关键词文件中没有找到有效关键词") return keywords except Exception as e: raise Exception(f"加载关键词文件失败: {str(e)}") def convert_to_wav(self, audio_file): """将音频文件转换为WAV格式(增强健壮性)""" try: if not os.path.exists(audio_file): raise FileNotFoundError(f"音频文件不存在: {audio_file}") if audio_file.lower().endswith('.wav'): return audio_file, False # 使用临时文件避免磁盘IO with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmpfile: output_file = tmpfile.name audio = AudioSegment.from_file(audio_file) audio.export(output_file, format='wav') return output_file, True except Exception as e: raise Exception(f"音频转换失败: {str(e)}") def get_audio_info(self, wav_file): """获取音频文件信息(增强健壮性)""" try: if not os.path.exists(wav_file): raise FileNotFoundError(f"音频文件不存在: {wav_file}") # 使用soundfile获取更可靠的信息 with sf.SoundFile(wav_file) as f: duration = len(f) / f.samplerate sample_rate = f.samplerate channels = f.channels return duration, sample_rate, channels except Exception as e: raise Exception(f"获取音频信息失败: {str(e)}") def transcribe_audio_segment(self, wav_file, start, end, model): """转录单个音频片段 - 优化内存使用""" # 使用pydub加载音频 audio = AudioSegment.from_wav(wav_file) # 转换为毫秒 start_ms = int(start * 1000) end_ms = int(end * 1000) segment_audio = audio[start_ms:end_ms] # 使用临时文件 with tempfile.NamedTemporaryFile(suffix='.wav') as tmpfile: segment_audio.export(tmpfile.name, format="wav") try: result = model.transcribe( tmpfile.name, fp16=cuda_available() # 使用FP16加速(如果可用) ) return result['text'] except RuntimeError as e: if "out of memory" in str(e).lower(): # 尝试释放内存后重试 torch.cuda.empty_cache() gc.collect() result = model.transcribe( tmpfile.name, fp16=cuda_available() ) return result['text'] raise def analyze_emotions(self, texts): """批量分析文本情感(提高效率)""" if not any(t.strip() for t in texts): return [{"label": "中性", "score": 0.0} for _ in texts] # 截断长文本以提高性能 processed_texts = [t[:500] if len(t) > 500 else t for t in texts] # 批量处理 classifier = self.cached_models['emotion_classifier'] results = classifier(processed_texts, truncation=True, max_length=512, batch_size=4) # 确保返回格式一致 emotions = [] for result in results: if isinstance(result, list) and result: emotions.append({ "label": result[0]['label'], "score": result[0]['score'] }) else: emotions.append({ "label": "中性", "score": 0.0 }) return emotions def check_opening(self, text, opening_keywords): """检查开场白(使用正则表达式提高准确性)""" if not text or not opening_keywords: return False pattern = "|".join(re.escape(k) for k in opening_keywords) return bool(re.search(pattern, text)) def check_closing(self, text, closing_keywords): """检查结束语(使用正则表达式提高准确性)""" if not text or not closing_keywords: return False pattern = "|".join(re.escape(k) for k in closing_keywords) return bool(re.search(pattern, text)) def check_forbidden(self, text, forbidden_keywords): """检查服务禁语(使用正则表达式提高准确性)""" if not text or not forbidden_keywords: return False pattern = "|".join(re.escape(k) for k in forbidden_keywords) return bool(re.search(pattern, text)) def analyze_speech_rate(self, segments): """改进的语速分析 - 基于实际识别文本""" if not segments: return 0 total_chars = 0 total_duration = 0 for segment in segments: # 计算片段时长(秒) duration = segment['duration'] total_duration += duration # 计算中文字符数(去除标点和空格) chinese_chars = sum(1 for char in segment['text'] if '\u4e00' <= char <= '\u9fff') total_chars += chinese_chars if total_duration == 0: return 0 # 语速 = 总字数 / 总时长(分钟) return total_chars / (total_duration / 60) def analyze_volume(self, wav_file, segments, sample_rate): """改进的音量分析 - 使用librosa计算RMS分贝值""" if not segments: return {"mean": -60, "std": 0} # 使用soundfile加载音频(更高效) try: y, sr = sf.read(wav_file, dtype='float32') if sr != sample_rate: y = librosa.resample(y, orig_sr=sr, target_sr=sample_rate) sr = sample_rate except Exception: # 回退到librosa y, sr = librosa.load(wav_file, sr=sample_rate, mono=True) all_dB = [] for start, end in segments: start_sample = int(start * sr) end_sample = int(end * sr) # 确保片段在有效范围内 if start_sample < len(y) and end_sample <= len(y): segment_audio = y[start_sample:end_sample] # 计算RMS并转换为dB rms = librosa.feature.rms(y=segment_audio)[0] dB = librosa.amplitude_to_db(rms, ref=1.0) # 使用标准参考值 all_dB.extend(dB) if not all_dB: return {"mean": -60, "std": 0} return { "mean": float(np.mean(all_dB)), "std": float(np.std(all_dB)) } def analyze_resolution(self, agent_text, customer_text, resolution_keywords): """分析问题解决率(使用更智能的匹配)""" # 检查客户是否提到问题 problem_patterns = [ "问题", "故障", "解决", "怎么办", "如何", "为什么", "不行", "不能", "无法", "错误", "bug", "issue", "疑问", "咨询" ] problem_regex = re.compile("|".join(problem_patterns)) has_problem = bool(problem_regex.search(customer_text)) # 检查客服是否提供解决方案 solution_regex = re.compile("|".join(re.escape(k) for k in resolution_keywords)) solution_found = bool(solution_regex.search(agent_text)) # 如果没有检测到问题,则认为已解决 if not has_problem: return True return solution_found def stop(self): """停止分析""" self.running = False self.message.emit("正在停止分析...") class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("外呼电话录音包质检分析系统") self.setGeometry(100, 100, 1000, 700) self.setStyleSheet(""" QMainWindow { background-color: #f0f0f0; } QGroupBox { font-weight: bold; border: 1px solid gray; border-radius: 5px; margin-top: 1ex; } QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; } QPushButton { background-color: #4CAF50; color: white; border: none; padding: 5px 10px; border-radius: 3px; } QPushButton:hover { background-color: #45a049; } QPushButton:disabled { background-color: #cccccc; } QProgressBar { border: 1px solid grey; border-radius: 3px; text-align: center; } QProgressBar::chunk { background-color: #4CAF50; width: 10px; } QTextEdit { font-family: Consolas, Monaco, monospace; } """) # 初始化变量 self.audio_files = [] self.keyword_file = "" self.whisper_model_path = "./models/whisper-small" self.pyannote_model_path = "./models/pyannote-speaker-diarization" self.emotion_model_path = "./models/Erlangshen-Roberta-110M-Sentiment" self.output_dir = os.path.expanduser("~/质检报告") # 创建主控件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(10) main_layout.setContentsMargins(15, 15, 15, 15) # 文件选择区域 file_group = QGroupBox("文件选择") file_layout = QVBoxLayout(file_group) file_layout.setSpacing(8) # 音频文件选择 audio_layout = QHBoxLayout() self.audio_label = QLabel("音频文件/文件夹:") audio_layout.addWidget(self.audio_label) self.audio_path_edit = QLineEdit() self.audio_path_edit.setPlaceholderText("请选择音频文件或文件夹") audio_layout.addWidget(self.audio_path_edit, 3) self.audio_browse_btn = QPushButton("浏览...") self.audio_browse_btn.clicked.connect(self.browse_audio) audio_layout.addWidget(self.audio_browse_btn) file_layout.addLayout(audio_layout) # 关键词文件选择 keyword_layout = QHBoxLayout() self.keyword_label = QLabel("关键词文件:") keyword_layout.addWidget(self.keyword_label) self.keyword_path_edit = QLineEdit() self.keyword_path_edit.setPlaceholderText("请选择Excel格式的关键词文件") keyword_layout.addWidget(self.keyword_path_edit, 3) self.keyword_browse_btn = QPushButton("浏览...") self.keyword_browse_btn.clicked.connect(self.browse_keyword) keyword_layout.addWidget(self.keyword_browse_btn) file_layout.addLayout(keyword_layout) main_layout.addWidget(file_group) # 模型设置区域 model_group = QGroupBox("模型设置") model_layout = QVBoxLayout(model_group) model_layout.setSpacing(8) # Whisper模型路径 whisper_layout = QHBoxLayout() whisper_layout.addWidget(QLabel("Whisper模型路径:")) self.whisper_edit = QLineEdit(self.whisper_model_path) whisper_layout.addWidget(self.whisper_edit, 3) model_layout.addLayout(whisper_layout) # Pyannote模型路径 pyannote_layout = QHBoxLayout() pyannote_layout.addWidget(QLabel("Pyannote模型路径:")) self.pyannote_edit = QLineEdit(self.pyannote_model_path) pyannote_layout.addWidget(self.pyannote_edit, 3) model_layout.addLayout(pyannote_layout) # 情感分析模型路径 emotion_layout = QHBoxLayout() emotion_layout.addWidget(QLabel("情感分析模型路径:")) self.emotion_edit = QLineEdit(self.emotion_model_path) emotion_layout.addWidget(self.emotion_edit, 3) model_layout.addLayout(emotion_layout) # 输出目录 output_layout = QHBoxLayout() output_layout.addWidget(QLabel("输出目录:")) self.output_edit = QLineEdit(self.output_dir) self.output_edit.setPlaceholderText("请选择报告输出目录") output_layout.addWidget(self.output_edit, 3) self.output_browse_btn = QPushButton("浏览...") self.output_browse_btn.clicked.connect(self.browse_output) output_layout.addWidget(self.output_browse_btn) model_layout.addLayout(output_layout) main_layout.addWidget(model_group) # 控制按钮区域 control_layout = QHBoxLayout() control_layout.setSpacing(10) self.start_btn = QPushButton("开始分析") self.start_btn.setStyleSheet("background-color: #2196F3;") self.start_btn.clicked.connect(self.start_analysis) control_layout.addWidget(self.start_btn) self.stop_btn = QPushButton("停止分析") self.stop_btn.setStyleSheet("background-color: #f44336;") self.stop_btn.clicked.connect(self.stop_analysis) self.stop_btn.setEnabled(False) control_layout.addWidget(self.stop_btn) self.clear_btn = QPushButton("清空") self.clear_btn.clicked.connect(self.clear_all) control_layout.addWidget(self.clear_btn) main_layout.addLayout(control_layout) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setValue(0) self.progress_bar.setFormat("就绪") self.progress_bar.setMinimumHeight(25) main_layout.addWidget(self.progress_bar) # 日志输出区域 log_group = QGroupBox("分析日志") log_layout = QVBoxLayout(log_group) self.log_text = QTextEdit() self.log_text.setReadOnly(True) log_layout.addWidget(self.log_text) main_layout.addWidget(log_group, 1) # 给日志区域更多空间 # 状态区域 status_layout = QHBoxLayout() self.status_label = QLabel("状态: 就绪") status_layout.addWidget(self.status_label, 1) self.file_count_label = QLabel("已选择0个音频文件") status_layout.addWidget(self.file_count_label) main_layout.addLayout(status_layout) # 初始化分析线程 self.analysis_thread = None def browse_audio(self): """浏览音频文件或文件夹""" options = QFileDialog.Options() files, _ = QFileDialog.getOpenFileNames( self, "选择音频文件", "", "音频文件 (*.mp3 *.wav *.amr *.ogg *.flac *.m4a);;所有文件 (*)", options=options ) if files: self.audio_files = files self.audio_path_edit.setText("; ".join(files)) self.file_count_label.setText(f"已选择{len(files)}个音频文件") self.log_text.append(f"已选择{len(files)}个音频文件") def browse_keyword(self): """浏览关键词文件""" options = QFileDialog.Options() file, _ = QFileDialog.getOpenFileName( self, "选择关键词文件", "", "Excel文件 (*.xlsx *.xls);;所有文件 (*)", options=options ) if file: self.keyword_file = file self.keyword_path_edit.setText(file) self.log_text.append(f"已选择关键词文件: {file}") def browse_output(self): """浏览输出目录""" options = QFileDialog.Options() directory = QFileDialog.getExistingDirectory( self, "选择输出目录", self.output_dir, options=options ) if directory: self.output_dir = directory self.output_edit.setText(directory) self.log_text.append(f"输出目录设置为: {directory}") def start_analysis(self): """开始分析""" if not self.audio_files: self.show_warning("请先选择音频文件") return if not self.keyword_file: self.show_warning("请先选择关键词文件") return if not os.path.exists(self.keyword_file): self.show_warning("关键词文件不存在,请重新选择") return # 检查模型路径 model_paths = [ self.whisper_edit.text(), self.pyannote_edit.text(), self.emotion_edit.text() ] for path in model_paths: if not os.path.exists(path): self.show_warning(f"模型路径不存在: {path}") return # 更新模型路径 self.whisper_model_path = self.whisper_edit.text() self.pyannote_model_path = self.pyannote_edit.text() self.emotion_model_path = self.emotion_edit.text() self.output_dir = self.output_edit.text() # 创建输出目录 os.makedirs(self.output_dir, exist_ok=True) self.log_text.append("开始分析...") self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.status_label.setText("状态: 分析中...") self.progress_bar.setFormat("分析中... 0%") self.progress_bar.setValue(0) # 创建并启动分析线程 self.analysis_thread = AnalysisThread( self.audio_files, self.keyword_file, self.whisper_model_path, self.pyannote_model_path, self.emotion_model_path ) self.analysis_thread.progress.connect(self.update_progress) self.analysis_thread.message.connect(self.log_text.append) self.analysis_thread.analysis_complete.connect(self.on_analysis_complete) self.analysis_thread.error.connect(self.on_analysis_error) self.analysis_thread.finished.connect(self.on_analysis_finished) self.analysis_thread.start() def update_progress(self, value): """更新进度条""" self.progress_bar.setValue(value) self.progress_bar.setFormat(f"分析中... {value}%") def stop_analysis(self): """停止分析""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.log_text.append("正在停止分析...") self.stop_btn.setEnabled(False) def clear_all(self): """清空所有内容""" self.audio_files = [] self.keyword_file = "" self.audio_path_edit.clear() self.keyword_path_edit.clear() self.log_text.clear() self.progress_bar.setValue(0) self.progress_bar.setFormat("就绪") self.status_label.setText("状态: 就绪") self.file_count_label.setText("已选择0个音频文件") self.log_text.append("已清空所有内容") def show_warning(self, message): """显示警告消息""" QMessageBox.warning(self, "警告", message) self.log_text.append(f"警告: {message}") def on_analysis_complete(self, result): """分析完成处理""" try: self.log_text.append("正在生成报告...") if not result.get("results"): self.log_text.append("警告: 没有生成任何分析结果") return # 生成Excel报告 excel_path = os.path.join(self.output_dir, "质检分析报告.xlsx") self.generate_excel_report(result, excel_path) # 生成Word报告 word_path = os.path.join(self.output_dir, "质检分析报告.docx") self.generate_word_report(result, word_path) self.log_text.append(f"分析报告已保存至: {excel_path}") self.log_text.append(f"可视化报告已保存至: {word_path}") self.log_text.append("分析完成!") self.status_label.setText(f"状态: 分析完成!报告保存至: {self.output_dir}") self.progress_bar.setFormat("分析完成!") # 显示完成消息 QMessageBox.information( self, "分析完成", f"分析完成!报告已保存至:\n{excel_path}\n{word_path}" ) except Exception as e: import traceback error_msg = f"生成报告时出错: {str(e)}\n{traceback.format_exc()}" self.log_text.append(error_msg) logger.error(error_msg) def on_analysis_error(self, message): """分析错误处理""" self.log_text.append(f"错误: {message}") self.status_label.setText("状态: 发生错误") self.progress_bar.setFormat("发生错误") QMessageBox.critical(self, "分析错误", message) def on_analysis_finished(self): """分析线程结束处理""" self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) def generate_excel_report(self, result, output_path): """生成Excel报告(增强健壮性)""" try: # 从结果中提取数据 data = [] for res in result['results']: data.append({ "文件名": res['file_name'], "音频时长(秒)": res['duration'], "开场白检查": "通过" if res['opening_check'] else "未通过", "结束语检查": "通过" if res['closing_check'] else "未通过", "服务禁语检查": "通过" if not res['forbidden_check'] else "未通过", "客服情感": res['agent_emotion']['label'], "客服情感得分": res['agent_emotion']['score'], "客户情感": res['customer_emotion']['label'], "客户情感得分": res['customer_emotion']['score'], "语速(字/分)": res['speech_rate'], "平均音量(dB)": res['volume_mean'], "音量标准差": res['volume_std'], "问题解决率": "是" if res['resolution_rate'] else "否" }) # 创建DataFrame并保存 df = pd.DataFrame(data) # 尝试使用openpyxl引擎(更稳定) try: df.to_excel(output_path, index=False, engine='openpyxl') except ImportError: df.to_excel(output_path, index=False) # 添加汇总统计 try: with pd.ExcelWriter(output_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer: summary_data = { "统计项": ["总文件数", "开场白通过率", "结束语通过率", "服务禁语通过率", "问题解决率"], "数值": [ len(result['results']), df['开场白检查'].value_counts().get('通过', 0) / len(df), df['结束语检查'].value_counts().get('通过', 0) / len(df), df['服务禁语检查'].value_counts().get('通过', 0) / len(df), df['问题解决率'].value_counts().get('是', 0) / len(df) ] } summary_df = pd.DataFrame(summary_data) summary_df.to_excel(writer, sheet_name='汇总统计', index=False) except Exception as e: self.log_text.append(f"添加汇总统计时出错: {str(e)}") except Exception as e: raise Exception(f"生成Excel报告失败: {str(e)}") def generate_word_report(self, result, output_path): """生成Word报告(增强健壮性)""" try: doc = Document() # 添加标题 doc.add_heading('外呼电话录音质检分析报告', 0) # 添加基本信息 doc.add_heading('分析概况', level=1) doc.add_paragraph(f"分析时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") doc.add_paragraph(f"分析文件数量: {len(result['results'])}") doc.add_paragraph(f"关键词文件: {os.path.basename(self.keyword_file)}") # 添加汇总统计 doc.add_heading('汇总统计', level=1) # 创建汇总表格 table = doc.add_table(rows=5, cols=2) table.style = 'Table Grid' # 表头 hdr_cells = table.rows[0].cells hdr_cells[0].text = '统计项' hdr_cells[1].text = '数值' # 计算统计数据 df = pd.DataFrame(result['results']) pass_rates = { "开场白通过率": df['opening_check'].mean() if not df.empty else 0, "结束语通过率": df['closing_check'].mean() if not df.empty else 0, "服务禁语通过率": (1 - df['forbidden_check']).mean() if not df.empty else 0, "问题解决率": df['resolution_rate'].mean() if not df.empty else 0 } # 填充表格 rows = [ ("总文件数", len(result['results'])), ("开场白通过率", f"{pass_rates['开场白通过率']:.2%}"), ("结束语通过率", f"{pass_rates['结束语通过率']:.2%}"), ("服务禁语通过率", f"{pass_rates['服务禁语通过率']:.2%}"), ("问题解决率", f"{pass_rates['问题解决率']:.2%}") ] for i, row_data in enumerate(rows): if i < len(table.rows): row_cells = table.rows[i].cells row_cells[0].text = row_data[0] row_cells[1].text = str(row_data[1]) # 添加情感分析图表 if result['results']: doc.add_heading('情感分析', level=1) # 客服情感分布 agent_emotions = [res['agent_emotion']['label'] for res in result['results']] agent_emotion_counts = pd.Series(agent_emotions).value_counts() if not agent_emotion_counts.empty: fig, ax = plt.subplots(figsize=(6, 4)) agent_emotion_counts.plot.pie(autopct='%1.1f%%', ax=ax) ax.set_title('客服情感分布') ax.set_ylabel('') # 移除默认的ylabel plt.tight_layout() # 保存图表到临时文件 chart_path = os.path.join(self.output_dir, "agent_emotion_chart.png") plt.savefig(chart_path, dpi=100, bbox_inches='tight') plt.close() doc.add_picture(chart_path, width=Inches(4)) doc.add_paragraph('图1: 客服情感分布') # 客户情感分布 customer_emotions = [res['customer_emotion']['label'] for res in result['results']] customer_emotion_counts = pd.Series(customer_emotions).value_counts() if not customer_emotion_counts.empty: fig, ax = plt.subplots(figsize=(6, 4)) customer_emotion_counts.plot.pie(autopct='%1.1f%%', ax=ax) ax.set_title('客户情感分布') ax.set_ylabel('') # 移除默认的ylabel plt.tight_layout() chart_path = os.path.join(self.output_dir, "customer_emotion_chart.png") plt.savefig(chart_path, dpi=100, bbox_inches='tight') plt.close() doc.add_picture(chart_path, width=Inches(4)) doc.add_paragraph('图2: 客户情感分布') # 添加详细分析结果 doc.add_heading('详细分析结果', level=1) # 创建详细表格 table = doc.add_table(rows=1, cols=6) table.style = 'Table Grid' # 表头 hdr_cells = table.rows[0].cells headers = ['文件名', '开场白', '结束语', '禁语', '客服情感', '问题解决'] for i, header in enumerate(headers): hdr_cells[i].text = header # 填充数据 for res in result['results']: row_cells = table.add_row().cells row_cells[0].text = res['file_name'] row_cells[1].text = "✓" if res['opening_check'] else "✗" row_cells[2].text = "✓" if res['closing_check'] else "✗" row_cells[3].text = "✗" if res['forbidden_check'] else "✓" row_cells[4].text = res['agent_emotion']['label'] row_cells[5].text = "✓" if res['resolution_rate'] else "✗" # 保存文档 doc.save(output_path) except Exception as e: raise Exception(f"生成Word报告失败: {str(e)}") if __name__ == "__main__": # 检查是否安装了torch try: import torch except ImportError: print("警告: PyTorch 未安装,情感分析可能无法使用GPU加速") app = QApplication(sys.argv) # 设置应用样式 app.setStyle("Fusion") window = MainWindow() window.show() sys.exit(app.exec_())

最新推荐

recommend-type

基于Android开发的闯关类小游戏项目源码文档

随着人们生活水平的提高和科技的不断发展,以智能手机为代表的移动电子产品逐渐普及,基于移动平台的游戏市场潜力巨大,受到商业投资者的关注。Java语言的面向对象优势使得基于Android平台的益智类割绳子小游戏得以实现。该游戏采用Jbox2d物理引擎,该引擎源版本为C++编写,后扩展至Java,便于在Android平台使用。该游戏设计基于Jbox2d物理引擎,通过大量类与控件构建出丰富的游戏世界。论文介绍了系统开发背景与研究现状,分析了游戏中使用的类与框架,详细介绍了关键技术,并对游戏程序进行了测试。游戏操作简单,只需滑动屏幕,易于上手。资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
recommend-type

材料科学中逆磁致伸缩效应对磁导率影响的研究及应用 v1.2

内容概要:本文详细介绍了逆磁致伸缩效应的基本概念、原理及其导致的磁导率变化。首先解释了逆磁致伸缩效应的概念,即材料在外加磁场作用下发生的尺寸或形状变化。接着阐述了其原理,指出外部磁场使材料内部磁畴重新排列从而引起形变。然后讨论了应变如何导致磁导率的变化,强调了这一现象在制造高灵敏度传感器、执行器和微电子机械系统(MEMS)中的应用潜力。最后展望了该领域未来的研究方向和发展前景。 适合人群:从事材料科学研究的专业人士、对磁性材料感兴趣的科研工作者和技术爱好者。 使用场景及目标:适用于希望深入了解逆磁致伸缩效应及其应用的研究人员,旨在帮助他们掌握相关理论知识并探索实际应用场景。 其他说明:文中提到的技术和应用对于开发新型功能性器件有着重要意义,特别是在传感技术和微机电系统的进步方面。
recommend-type

工业自动化领域PLC远程监控调试:基于多线程Socket通讯与WIFI模块对接的技术实现

内容概要:本文详细介绍了如何利用多线程Socket通讯技术和WIFI模块实现对PLC(可编程逻辑控制器)的远程监控和调试。首先阐述了工业自动化背景下PLC远程监控的重要性和挑战,接着深入探讨了多客户端TCP中转服务器的设计与实现,包括Socket多线程并发通讯的具体应用,确保多个PLC设备能稳定地与远程监控系统相连。此外,还讨论了如何通过WIFI模块将PLC的串口数据传输到中转服务器,解决数据传输中的实时性、稳定性和安全性问题。最后,针对不同品牌的PLC设备及其配套硬件,提出了兼容性的解决方案,如设计适配器和驱动程序来支持各类串口服务器及TCP以太网转发器硬件。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是那些需要掌握PLC远程监控和调试技能的专业人士。 使用场景及目标:适用于需要对分布在广阔区域内的PLC设备进行集中管理和实时监控的企业环境。主要目标是提高PLC系统的可维护性、效率和可靠性,减少现场维护成本。 其他说明:文中提供的技术方案不仅限于理论探讨,还包括实际操作层面的内容,如具体的编码实现和硬件配置建议,有助于读者快速理解和应用。
recommend-type

Matlab环境下SIFT与Meanshift融合的视频目标识别与追踪技术

内容概要:本文详细介绍了在Matlab环境中,如何利用SIFT(尺度不变特征变换)算法和Meanshift算法进行视频中的目标识别与追踪。文中首先解释了两种算法的基本原理,接着阐述了它们的融合应用,即先用SIFT算法提取目标的关键点并生成描述符,再通过Meanshift算法计算目标在连续帧间的位移,实现精准追踪。此外,还提到了帧差分法的应用,通过对比相邻帧的差异来检测运动目标,进一步提升识别与追踪的效果。最后,通过对实际视频数据的测试验证了该方法的有效性。 适合人群:从事计算机视觉、图像处理领域的研究人员和技术开发者,尤其是那些希望深入了解SIFT和Meanshift算法及其在Matlab环境下的应用的人群。 使用场景及目标:适用于智能视频监控系统的设计与开发,旨在提高目标识别与追踪的准确性和鲁棒性。通过融合多种算法,可以在复杂多变的环境中实现高效的目标检测和持续跟踪。 其他说明:文中提供的Matlab代码示例展示了具体的实现步骤,有助于读者更好地理解和实践相关技术。
recommend-type

基于Simulink的14自由度汽车动力学模型:侧倾行为及多工况仿真研究

内容概要:本文详细介绍了基于Simulink构建的14自由度汽车动力学模型,用于模拟不同驾驶条件下的车辆动态性能,特别是侧倾行为。模型涵盖了车体、悬架和轮胎的动力学特性,采用经典的魔术轮胎公式进行轮胎建模,并通过状态空间方程描述悬架系统的动态行为。输入变量包括方向盘转角、节气门开度、制动踏板位置和初始速度,输出则涵盖纵向速度、横摆角速度、横向加速度、轮胎滑移率和侧倾角等多种状态信息。模型经过阶跃转向和双移线工况验证,确保了其准确性并与CarSim进行了对比,证明了其优越的仿真效果。 适合人群:从事汽车工程、车辆动力学研究的专业人士,尤其是那些需要深入了解车辆侧倾行为及其仿真的研究人员和技术人员。 使用场景及目标:①研究不同工况下的车辆动力学特性;②评估和改进车辆悬挂系统的设计;③优化轮胎模型以提高仿真精度;④验证和调整模型参数以适应特定车型的需求。 其他说明:文中提供了详细的模型结构解析和关键代码片段,有助于读者理解和复现该模型。同时,还提到了一些潜在的改进方向和注意事项,如非线性刚度曲线的应用和数值稳定性的保持。
recommend-type

掌握XFireSpring整合技术:HELLOworld原代码使用教程

标题:“xfirespring整合使用原代码”中提到的“xfirespring”是指将XFire和Spring框架进行整合使用。XFire是一个基于SOAP的Web服务框架,而Spring是一个轻量级的Java/Java EE全功能栈的应用程序框架。在Web服务开发中,将XFire与Spring整合能够发挥两者的优势,例如Spring的依赖注入、事务管理等特性,与XFire的简洁的Web服务开发模型相结合。 描述:“xfirespring整合使用HELLOworld原代码”说明了在这个整合过程中实现了一个非常基本的Web服务示例,即“HELLOworld”。这通常意味着创建了一个能够返回"HELLO world"字符串作为响应的Web服务方法。这个简单的例子用来展示如何设置环境、编写服务类、定义Web服务接口以及部署和测试整合后的应用程序。 标签:“xfirespring”表明文档、代码示例或者讨论集中于XFire和Spring的整合技术。 文件列表中的“index.jsp”通常是一个Web应用程序的入口点,它可能用于提供一个用户界面,通过这个界面调用Web服务或者展示Web服务的调用结果。“WEB-INF”是Java Web应用中的一个特殊目录,它存放了应用服务器加载的Servlet类文件和相关的配置文件,例如web.xml。web.xml文件中定义了Web应用程序的配置信息,如Servlet映射、初始化参数、安全约束等。“META-INF”目录包含了元数据信息,这些信息通常由部署工具使用,用于描述应用的元数据,如manifest文件,它记录了归档文件中的包信息以及相关的依赖关系。 整合XFire和Spring框架,具体知识点可以分为以下几个部分: 1. XFire框架概述 XFire是一个开源的Web服务框架,它是基于SOAP协议的,提供了一种简化的方式来创建、部署和调用Web服务。XFire支持多种数据绑定,包括XML、JSON和Java数据对象等。开发人员可以使用注解或者基于XML的配置来定义服务接口和服务实现。 2. Spring框架概述 Spring是一个全面的企业应用开发框架,它提供了丰富的功能,包括但不限于依赖注入、面向切面编程(AOP)、数据访问/集成、消息传递、事务管理等。Spring的核心特性是依赖注入,通过依赖注入能够将应用程序的组件解耦合,从而提高应用程序的灵活性和可测试性。 3. XFire和Spring整合的目的 整合这两个框架的目的是为了利用各自的优势。XFire可以用来创建Web服务,而Spring可以管理这些Web服务的生命周期,提供企业级服务,如事务管理、安全性、数据访问等。整合后,开发者可以享受Spring的依赖注入、事务管理等企业级功能,同时利用XFire的简洁的Web服务开发模型。 4. XFire与Spring整合的基本步骤 整合的基本步骤可能包括添加必要的依赖到项目中,配置Spring的applicationContext.xml,以包括XFire特定的bean配置。比如,需要配置XFire的ServiceExporter和ServicePublisher beans,使得Spring可以管理XFire的Web服务。同时,需要定义服务接口以及服务实现类,并通过注解或者XML配置将其关联起来。 5. Web服务实现示例:“HELLOworld” 实现一个Web服务通常涉及到定义服务接口和服务实现类。服务接口定义了服务的方法,而服务实现类则提供了这些方法的具体实现。在XFire和Spring整合的上下文中,“HELLOworld”示例可能包含一个接口定义,比如`HelloWorldService`,和一个实现类`HelloWorldServiceImpl`,该类有一个`sayHello`方法返回"HELLO world"字符串。 6. 部署和测试 部署Web服务时,需要将应用程序打包成WAR文件,并部署到支持Servlet 2.3及以上版本的Web应用服务器上。部署后,可以通过客户端或浏览器测试Web服务的功能,例如通过访问XFire提供的服务描述页面(WSDL)来了解如何调用服务。 7. JSP与Web服务交互 如果在应用程序中使用了JSP页面,那么JSP可以用来作为用户与Web服务交互的界面。例如,JSP可以包含JavaScript代码来发送异步的AJAX请求到Web服务,并展示返回的结果给用户。在这个过程中,JSP页面可能使用XMLHttpRequest对象或者现代的Fetch API与Web服务进行通信。 8. 项目配置文件说明 项目配置文件如web.xml和applicationContext.xml分别在Web应用和服务配置中扮演关键角色。web.xml负责定义Web组件,比如Servlet、过滤器和监听器,而applicationContext.xml则负责定义Spring容器中的bean,包括数据源、事务管理器、业务逻辑组件和服务访问器等。 总之,通过上述整合使用原代码的知识点,可以深入理解XFire与Spring框架的结合使用,以及如何开发和部署基本的Web服务。这些技术知识有助于进行更高层次的Web服务开发,以及在复杂的IT环境中灵活运用各种框架和工具。
recommend-type

【Unity2018汉化大揭秘】:一步到位优化中文用户体验

# 摘要 本论文详细介绍了Unity2018汉化项目的实施过程,从理论准备到实践步骤,再到后期的测试与优化,最终分享了汉化经验和心得。首先,强调了汉化项目对于用户体验提升和产品国际化的重要性,并分析了汉化资源的分类、识别与管理方法。接着,详细阐述了汉化工具的选择、脚本编写、资源手动调整等实践步骤,以及汉化质量测试、体验优化和项目维护更新的重要性。论文最后通过案例研究,分析了汉化中的挑战和应对策略,并预测了汉化技术未来的发展方向。整个汉化过程不仅涉及技术层面的操作,还包含了深入的理论研究和实践心得分享,对提升汉化项目的效率和质量具有指导意义。 # 关键字 Unity汉化;用户体验;国际化;资源
recommend-type

iPhone

<think>我们注意到用户意图不明确,但提到了“照片提取”和“其他功能帮助”。因此,我们需要通过搜索来获取关于iPhone照片提取的常见方法以及其他可能的功能帮助。由于用户问题比较宽泛,我们将重点放在照片提取上,因为这是明确提到的关键词。同时,我们也会考虑一些其他常用功能的帮助。首先,针对照片提取,可能涉及从iPhone导出照片、从备份中提取照片、或者从损坏的设备中恢复照片等。我们将搜索这些方面的信息。其次,关于其他功能帮助,我们可以提供一些常见问题的快速指南,如电池优化、屏幕时间管理等。根据要求,我们需要将答案组织为多个方法或步骤,并在每个步骤间换行。同时,避免使用第一人称和步骤词汇。由于
recommend-type

驾校一点通软件:提升驾驶证考试通过率

标题“驾校一点通”指向的是一款专门为学员考取驾驶证提供帮助的软件,该软件强调其辅助性质,旨在为学员提供便捷的学习方式和复习资料。从描述中可以推断出,“驾校一点通”是一个与驾驶考试相关的应用软件,这类软件一般包含驾驶理论学习、模拟考试、交通法规解释等内容。 文件标题中的“2007”这个年份标签很可能意味着软件的最初发布时间或版本更新年份,这说明了软件具有一定的历史背景和可能经过了多次更新,以适应不断变化的驾驶考试要求。 压缩包子文件的文件名称列表中,有以下几个文件类型值得关注: 1. images.dat:这个文件名表明,这是一个包含图像数据的文件,很可能包含了用于软件界面展示的图片,如各种标志、道路场景等图形。在驾照学习软件中,这类图片通常用于帮助用户认识和记忆不同交通标志、信号灯以及驾驶过程中需要注意的各种道路情况。 2. library.dat:这个文件名暗示它是一个包含了大量信息的库文件,可能包含了法规、驾驶知识、考试题库等数据。这类文件是提供给用户学习驾驶理论知识和准备科目一理论考试的重要资源。 3. 驾校一点通小型汽车专用.exe:这是一个可执行文件,是软件的主要安装程序。根据标题推测,这款软件主要是针对小型汽车驾照考试的学员设计的。通常,小型汽车(C1类驾照)需要学习包括车辆构造、基础驾驶技能、安全行车常识、交通法规等内容。 4. 使用说明.html:这个文件是软件使用说明的文档,通常以网页格式存在,用户可以通过浏览器阅读。使用说明应该会详细介绍软件的安装流程、功能介绍、如何使用软件的各种模块以及如何通过软件来帮助自己更好地准备考试。 综合以上信息,我们可以挖掘出以下几个相关知识点: - 软件类型:辅助学习软件,专门针对驾驶考试设计。 - 应用领域:主要用于帮助驾考学员准备理论和实践考试。 - 文件类型:包括图片文件(images.dat)、库文件(library.dat)、可执行文件(.exe)和网页格式的说明文件(.html)。 - 功能内容:可能包含交通法规知识学习、交通标志识别、驾驶理论学习、模拟考试、考试题库练习等功能。 - 版本信息:软件很可能最早发布于2007年,后续可能有多个版本更新。 - 用户群体:主要面向小型汽车驾照考生,即C1类驾照学员。 - 使用方式:用户需要将.exe安装文件进行安装,然后根据.html格式的使用说明来熟悉软件操作,从而利用images.dat和library.dat中的资源来辅助学习。 以上知识点为从给定文件信息中提炼出来的重点,这些内容对于了解“驾校一点通”这款软件的功能、作用、使用方法以及它的发展历史都有重要的指导意义。
recommend-type

【DFLauncher自动化教程】:简化游戏启动流程,让游戏体验更流畅

# 摘要 DFLauncher是一个功能丰富的游戏启动和管理平台,本论文将介绍其安装、基础使用、高级设置、社区互动以及插件开发等方面。通过对配置文件的解析、界面定制、自动化功能的实现、高级配置选项、安全性和性能监控的详细讨论,本文阐述了DFLauncher如何帮助用户更高效地管理和优化游戏环境。此外,本文还探讨了DFLauncher社区的资源分享、教育教程和插件开发等内容,