2026-10-12 16:30:00 +08:00
|
|
|
|
// 图形界面版:悬浮字幕窗 + 托盘 + 设置对话框。
|
|
|
|
|
|
// 完全通过 GUI 使用;命令行参数保留,仅用于自动化测试(显式给出时覆盖设置)。
|
2026-10-12 13:43:50 +08:00
|
|
|
|
#include <cstdio>
|
2026-10-12 16:30:00 +08:00
|
|
|
|
#include <memory>
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
#include <QAction>
|
2026-10-12 13:43:50 +08:00
|
|
|
|
#include <QApplication>
|
|
|
|
|
|
#include <QCommandLineOption>
|
|
|
|
|
|
#include <QCommandLineParser>
|
2026-10-12 16:30:00 +08:00
|
|
|
|
#include <QIcon>
|
|
|
|
|
|
#include <QMenu>
|
|
|
|
|
|
#include <QMessageBox>
|
|
|
|
|
|
#include <QPainter>
|
|
|
|
|
|
#include <QPixmap>
|
2026-10-12 17:03:20 +08:00
|
|
|
|
#include <QPushButton>
|
2026-10-12 16:30:00 +08:00
|
|
|
|
#include <QSystemTrayIcon>
|
2026-10-12 13:43:50 +08:00
|
|
|
|
#include <QTimer>
|
|
|
|
|
|
|
|
|
|
|
|
#include "appcli.h"
|
2026-10-12 16:30:00 +08:00
|
|
|
|
#include "appsettings.h"
|
2026-10-12 13:43:50 +08:00
|
|
|
|
#include "captionwindow.h"
|
2026-10-12 16:06:51 +08:00
|
|
|
|
#include "iasr.h"
|
2026-10-12 16:30:00 +08:00
|
|
|
|
#include "livetranscriber.h"
|
|
|
|
|
|
#include "modelstore.h"
|
2026-10-12 13:43:50 +08:00
|
|
|
|
#include "qwen3asr.h"
|
2026-10-12 16:30:00 +08:00
|
|
|
|
#include "settingsdialog.h"
|
2026-10-12 13:43:50 +08:00
|
|
|
|
#include "streamingasr.h"
|
|
|
|
|
|
#include "transcript.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
// 托盘图标:直接画一个,免得依赖图标资源文件
|
|
|
|
|
|
QIcon makeTrayIcon()
|
2026-10-12 13:43:50 +08:00
|
|
|
|
{
|
2026-10-12 16:30:00 +08:00
|
|
|
|
QPixmap pm(64, 64);
|
|
|
|
|
|
pm.fill(Qt::transparent);
|
|
|
|
|
|
QPainter p(&pm);
|
|
|
|
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
|
|
|
|
p.setBrush(QColor(20, 20, 20, 235));
|
|
|
|
|
|
p.setPen(Qt::NoPen);
|
|
|
|
|
|
p.drawRoundedRect(2, 12, 60, 40, 10, 10);
|
|
|
|
|
|
p.setPen(QColor(240, 240, 240));
|
|
|
|
|
|
QFont f = p.font();
|
|
|
|
|
|
f.setPixelSize(26);
|
|
|
|
|
|
f.setBold(true);
|
|
|
|
|
|
p.setFont(f);
|
|
|
|
|
|
p.drawText(pm.rect(), Qt::AlignCenter, QStringLiteral("字"));
|
|
|
|
|
|
return QIcon(pm);
|
|
|
|
|
|
}
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
// 把设置应用到字幕窗(字号/不透明度/行数/位置)
|
|
|
|
|
|
void applyAppearance(lc::CaptionWindow & window, const lc::AppSettings & cfg)
|
|
|
|
|
|
{
|
|
|
|
|
|
window.setPanelOpacity(cfg.opacityPercent);
|
|
|
|
|
|
window.setTextFontSize(cfg.fontSize);
|
|
|
|
|
|
window.setLineCounts(cfg.collapsedLines, cfg.expandedLines);
|
|
|
|
|
|
if (cfg.hasWindowPos) window.move(cfg.windowPos);
|
|
|
|
|
|
}
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
bool hasUsableModel()
|
|
|
|
|
|
{
|
|
|
|
|
|
using namespace lc::models;
|
|
|
|
|
|
const lc::AppSettings & cfg = lc::AppSettings::instance();
|
|
|
|
|
|
if (cfg.modelId.isEmpty()) return false;
|
|
|
|
|
|
const ModelInfo * m = ModelStore::find(cfg.modelId);
|
|
|
|
|
|
return m && ModelStore::isInstalled(*m);
|
2026-10-12 13:43:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
int main(int argc, char ** argv)
|
|
|
|
|
|
{
|
|
|
|
|
|
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
|
|
|
|
|
|
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
|
|
|
|
|
QApplication app(argc, argv);
|
2026-10-12 17:03:20 +08:00
|
|
|
|
// 关键:本程序常驻托盘,"最后一个窗口关闭"不等于退出。
|
|
|
|
|
|
// 否则首次启动时设置对话框一关(那时它是唯一窗口),Qt 就会把主事件循环也结束掉,
|
|
|
|
|
|
// 表现为"点了确定程序就自己退出"。
|
|
|
|
|
|
QApplication::setQuitOnLastWindowClosed(false);
|
2026-10-12 13:43:50 +08:00
|
|
|
|
QCoreApplication::setApplicationName(QStringLiteral("live-caption"));
|
2026-10-12 16:30:00 +08:00
|
|
|
|
QCoreApplication::setOrganizationName(QStringLiteral("live-caption"));
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
lc::AppSettings & cfg = lc::AppSettings::instance();
|
|
|
|
|
|
cfg.load();
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 命令行(仅用于自动化测试,显式给出时覆盖设置)----
|
2026-10-12 13:43:50 +08:00
|
|
|
|
QCommandLineParser parser;
|
|
|
|
|
|
parser.setApplicationDescription(QStringLiteral("实时字幕(悬浮窗)"));
|
|
|
|
|
|
parser.addHelpOption();
|
|
|
|
|
|
lc::app::addModelOptions(parser);
|
|
|
|
|
|
lc::app::addStreamOptions(parser);
|
2026-10-12 16:06:51 +08:00
|
|
|
|
lc::app::addBackendOptions(parser);
|
2026-10-12 16:30:00 +08:00
|
|
|
|
parser.addOption(QCommandLineOption("opacity", "背景不透明度(20-100)", "n"));
|
|
|
|
|
|
parser.addOption(QCommandLineOption("screenshot", "启动后自动截图并退出", "path"));
|
|
|
|
|
|
parser.addOption(QCommandLineOption("screenshot-delay", "截图前等待秒数", "sec", "6"));
|
|
|
|
|
|
parser.addOption(QCommandLineOption("no-tray", "不创建托盘图标(测试用)"));
|
|
|
|
|
|
parser.addOption(QCommandLineOption("screenshot-settings", "打开设置对话框截图后退出", "path"));
|
2026-10-12 17:29:45 +08:00
|
|
|
|
parser.addOption(QCommandLineOption("settings-autoclose", "复现用:自动操作设置对话框(reject|close|accept|ok|apply)", "how"));
|
2026-10-12 17:03:20 +08:00
|
|
|
|
parser.addOption(QCommandLineOption("quit-after", "运行若干秒后优雅退出(测试退出路径用)", "sec"));
|
2026-10-12 13:43:50 +08:00
|
|
|
|
parser.process(app);
|
|
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
if (parser.isSet("asr-model")) cfg.modelId = parser.value("asr-model");
|
|
|
|
|
|
if (parser.isSet("language")) cfg.language = parser.value("language");
|
|
|
|
|
|
if (parser.isSet("opacity")) cfg.opacityPercent = parser.value("opacity").toInt();
|
|
|
|
|
|
if (parser.isSet("model-dir")) lc::models::ModelStore::setRootOverride(parser.value("model-dir"));
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
// 仅用于验证渲染:打开设置对话框截图后退出
|
|
|
|
|
|
if (parser.isSet("screenshot-settings")) {
|
|
|
|
|
|
const QString path = parser.value("screenshot-settings");
|
2026-10-12 17:03:20 +08:00
|
|
|
|
lc::SettingsDialog dlg(!hasUsableModel());
|
|
|
|
|
|
dlg.show();
|
|
|
|
|
|
QTimer::singleShot(1200, &dlg, [&dlg, path] {
|
|
|
|
|
|
dlg.grab().save(path);
|
2026-10-12 16:30:00 +08:00
|
|
|
|
std::fprintf(stderr, "设置对话框截图: %s\n", qUtf8Printable(path));
|
2026-10-12 17:03:20 +08:00
|
|
|
|
dlg.accept();
|
2026-10-12 16:30:00 +08:00
|
|
|
|
});
|
2026-10-12 17:03:20 +08:00
|
|
|
|
dlg.exec();
|
|
|
|
|
|
return 0;
|
2026-10-12 16:30:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-12 17:03:20 +08:00
|
|
|
|
// 复现用:把"点 ✕ / 点确定"自动化,用来定位崩溃
|
|
|
|
|
|
const QString autoclose = parser.value("settings-autoclose");
|
|
|
|
|
|
const auto scheduleAutoclose = [&autoclose](QDialog & dlg) {
|
|
|
|
|
|
if (autoclose.isEmpty()) return;
|
|
|
|
|
|
QTimer::singleShot(700, &dlg, [&dlg, &autoclose] {
|
|
|
|
|
|
if (autoclose == QLatin1String("reject")) dlg.reject();
|
|
|
|
|
|
else if (autoclose == QLatin1String("close")) dlg.close(); // 标题栏 ✕
|
|
|
|
|
|
else if (autoclose == QLatin1String("accept")) dlg.accept();
|
|
|
|
|
|
else if (autoclose == QLatin1String("ok")) { // 真的点「确定」
|
|
|
|
|
|
for (auto * b : dlg.findChildren<QPushButton *>()) {
|
|
|
|
|
|
if (b->text() == QObject::tr("确定")) { b->click(); break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-12 17:18:16 +08:00
|
|
|
|
else if (autoclose == QLatin1String("apply")) { // 点「应用」,应保持打开
|
|
|
|
|
|
for (auto * b : dlg.findChildren<QPushButton *>()) {
|
|
|
|
|
|
if (b->text() == QObject::tr("应用")) { b->click(); break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
QTimer::singleShot(400, &dlg, [&dlg] {
|
|
|
|
|
|
std::fprintf(stderr, "点「应用」后对话框仍可见: %s\n",
|
|
|
|
|
|
dlg.isVisible() ? "是 ✅" : "否 ❌");
|
|
|
|
|
|
dlg.reject();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-10-12 17:03:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
// ---- 启动:没有可用模型就先开设置 ----
|
|
|
|
|
|
if (!hasUsableModel()) {
|
|
|
|
|
|
lc::SettingsDialog dlg(true);
|
2026-10-12 17:03:20 +08:00
|
|
|
|
scheduleAutoclose(dlg);
|
2026-10-12 16:30:00 +08:00
|
|
|
|
if (dlg.exec() != QDialog::Accepted || !hasUsableModel()) return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lc::CaptionWindow window;
|
|
|
|
|
|
applyAppearance(window, cfg);
|
|
|
|
|
|
// 语言 pill:流式模型不提供语言识别,就显示设置里选的语言
|
|
|
|
|
|
if (!cfg.language.isEmpty()) {
|
|
|
|
|
|
window.setLanguage(QString::fromStdString(lc::languageDisplayName(cfg.language.toStdString())));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 注意声明顺序:asr 必须在 live 之前声明,
|
|
|
|
|
|
// 这样析构时 live(会 join 工作线程)先销毁、asr 后销毁,避免 use-after-free。
|
|
|
|
|
|
std::unique_ptr<lc::IAsr> asr;
|
|
|
|
|
|
lc::LiveTranscriber live; // 后端在 startTranscriber() 里注入
|
|
|
|
|
|
const auto startTranscriber = [&]() -> bool {
|
|
|
|
|
|
live.stop();
|
|
|
|
|
|
asr.reset();
|
2026-10-12 17:03:20 +08:00
|
|
|
|
live.setCaptureDevice(cfg.captureDevice);
|
2026-10-12 16:30:00 +08:00
|
|
|
|
|
|
|
|
|
|
const lc::models::ModelInfo * m = lc::models::ModelStore::find(cfg.modelId);
|
|
|
|
|
|
if (!m) {
|
|
|
|
|
|
qCritical("找不到模型 %s", qUtf8Printable(cfg.modelId));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string err;
|
|
|
|
|
|
asr = lc::app::makeAsrForModel(*m, cfg.language, cfg.threads,
|
|
|
|
|
|
parser.isSet("verbose"), &err);
|
|
|
|
|
|
if (!asr) {
|
|
|
|
|
|
qCritical("加载模型失败: %s", err.c_str());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lc::StreamOptions sop;
|
|
|
|
|
|
if (asr->kind() == lc::AsrKind::NativeStreaming) {
|
|
|
|
|
|
sop.chunkSec = (cfg.chunkSec > 0.0) ? cfg.chunkSec : 0.32;
|
|
|
|
|
|
} else if (cfg.chunkSec > 0.0) {
|
|
|
|
|
|
sop.chunkSec = cfg.chunkSec;
|
|
|
|
|
|
}
|
|
|
|
|
|
QString qerr;
|
|
|
|
|
|
if (!live.start(*asr, sop, &qerr)) {
|
|
|
|
|
|
qCritical("启动采集失败: %s", qUtf8Printable(qerr));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
QObject::connect(&live, &lc::LiveTranscriber::caption, &window,
|
|
|
|
|
|
[&window](const QString & committed, const QString & tail,
|
|
|
|
|
|
const QString & lang) {
|
|
|
|
|
|
if (!lang.isEmpty()) {
|
|
|
|
|
|
window.setLanguage(
|
|
|
|
|
|
QString::fromStdString(lc::languageDisplayName(lang.toStdString())));
|
|
|
|
|
|
}
|
|
|
|
|
|
window.setCaption(committed, tail);
|
|
|
|
|
|
});
|
|
|
|
|
|
QObject::connect(&live, &lc::LiveTranscriber::failed, &window, [](const QString & m) {
|
|
|
|
|
|
qWarning("采集失败: %s", qUtf8Printable(m));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-12 17:18:16 +08:00
|
|
|
|
// 后端相关项(模型/语言/采集设备)变化才需要重启;外观类改动即时生效
|
|
|
|
|
|
const auto backendKey = [&cfg] {
|
|
|
|
|
|
return cfg.modelId + QLatin1Char('|') + cfg.language + QLatin1Char('|') + cfg.captureDevice;
|
|
|
|
|
|
};
|
|
|
|
|
|
QString activeKey = backendKey();
|
|
|
|
|
|
|
|
|
|
|
|
const auto openSettings = [&] {
|
2026-10-12 16:30:00 +08:00
|
|
|
|
lc::SettingsDialog dlg(false);
|
2026-10-12 17:18:16 +08:00
|
|
|
|
QObject::connect(&dlg, &lc::SettingsDialog::settingsApplied, &window, [&] {
|
|
|
|
|
|
applyAppearance(window, cfg); // 透明度/字号等立刻可见
|
|
|
|
|
|
if (backendKey() != activeKey) {
|
|
|
|
|
|
if (startTranscriber()) activeKey = backendKey();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-10-12 16:30:00 +08:00
|
|
|
|
if (dlg.exec() == QDialog::Accepted) {
|
|
|
|
|
|
applyAppearance(window, cfg);
|
2026-10-12 17:18:16 +08:00
|
|
|
|
if (backendKey() != activeKey) {
|
|
|
|
|
|
if (!startTranscriber()) {
|
|
|
|
|
|
QMessageBox::warning(&window, QObject::tr("启动失败"),
|
|
|
|
|
|
QObject::tr("模型加载失败,请检查设置。"));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
activeKey = backendKey();
|
|
|
|
|
|
}
|
2026-10-12 16:30:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-12 17:18:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 语言 pill → 设置
|
|
|
|
|
|
QObject::connect(&window, &lc::CaptionWindow::languageClicked, &window, openSettings);
|
2026-10-12 16:30:00 +08:00
|
|
|
|
|
|
|
|
|
|
// ---- 托盘 ----
|
|
|
|
|
|
QSystemTrayIcon * tray = nullptr;
|
|
|
|
|
|
QMenu * trayMenu = nullptr;
|
|
|
|
|
|
if (!parser.isSet("no-tray")) {
|
|
|
|
|
|
tray = new QSystemTrayIcon(makeTrayIcon(), &app);
|
|
|
|
|
|
tray->setToolTip(QObject::tr("实时字幕"));
|
2026-10-12 17:03:20 +08:00
|
|
|
|
trayMenu = new QMenu(&window); // 必须有归属,否则退出时是悬空顶层窗口
|
2026-10-12 16:30:00 +08:00
|
|
|
|
auto * showAct = trayMenu->addAction(QObject::tr("显示/隐藏字幕"));
|
|
|
|
|
|
auto * setAct = trayMenu->addAction(QObject::tr("设置…"));
|
|
|
|
|
|
trayMenu->addSeparator();
|
|
|
|
|
|
auto * quitAct = trayMenu->addAction(QObject::tr("退出"));
|
|
|
|
|
|
tray->setContextMenu(trayMenu);
|
|
|
|
|
|
|
|
|
|
|
|
QObject::connect(showAct, &QAction::triggered, &window,
|
|
|
|
|
|
[&window] { window.setVisible(!window.isVisible()); });
|
2026-10-12 17:18:16 +08:00
|
|
|
|
QObject::connect(setAct, &QAction::triggered, &window, openSettings);
|
2026-10-12 16:30:00 +08:00
|
|
|
|
QObject::connect(quitAct, &QAction::triggered, &app, &QApplication::quit);
|
|
|
|
|
|
QObject::connect(tray, &QSystemTrayIcon::activated, &window,
|
|
|
|
|
|
[&window](QSystemTrayIcon::ActivationReason r) {
|
|
|
|
|
|
if (r == QSystemTrayIcon::Trigger) {
|
|
|
|
|
|
window.setVisible(!window.isVisible());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
tray->show();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭按钮 = 隐藏到托盘(否则关掉就没法再打开了)
|
|
|
|
|
|
QObject::connect(&window, &lc::CaptionWindow::closed, &window, [&] {
|
|
|
|
|
|
if (tray) {
|
|
|
|
|
|
window.hide();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
qApp->quit();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 记住拖动后的位置
|
|
|
|
|
|
QObject::connect(&window, &lc::CaptionWindow::moved, &window, [&](const QPoint & p) {
|
|
|
|
|
|
cfg.windowPos = p;
|
|
|
|
|
|
cfg.hasWindowPos = true;
|
|
|
|
|
|
cfg.save();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!startTranscriber()) {
|
|
|
|
|
|
QMessageBox::critical(nullptr, QObject::tr("启动失败"),
|
|
|
|
|
|
QObject::tr("无法加载模型,请检查设置。"));
|
2026-10-12 16:06:51 +08:00
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
2026-10-12 16:30:00 +08:00
|
|
|
|
|
|
|
|
|
|
window.show();
|
|
|
|
|
|
|
2026-10-12 17:03:20 +08:00
|
|
|
|
// 复现用:程序起来后再开一次设置并自动关闭(模拟从语言标签/托盘进设置)
|
|
|
|
|
|
if (!autoclose.isEmpty() && hasUsableModel()) {
|
|
|
|
|
|
QTimer::singleShot(2500, &window, [&] {
|
|
|
|
|
|
lc::SettingsDialog dlg(false);
|
|
|
|
|
|
scheduleAutoclose(dlg);
|
|
|
|
|
|
if (dlg.exec() == QDialog::Accepted) {
|
|
|
|
|
|
applyAppearance(window, cfg);
|
|
|
|
|
|
if (!startTranscriber()) qWarning("重启转写失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
QTimer::singleShot(500, qApp, &QCoreApplication::quit);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
// 自截图(验证渲染用)
|
|
|
|
|
|
if (parser.isSet("screenshot")) {
|
|
|
|
|
|
const QString path = parser.value("screenshot");
|
|
|
|
|
|
QTimer::singleShot(parser.value("screenshot-delay").toInt() * 1000, &window,
|
|
|
|
|
|
[&window, path] {
|
|
|
|
|
|
const bool ok1 = window.grab().save(path);
|
|
|
|
|
|
window.setCollapsed(false);
|
|
|
|
|
|
QString expanded = path;
|
|
|
|
|
|
expanded.replace(QStringLiteral(".png"), QStringLiteral("-expanded.png"));
|
|
|
|
|
|
const bool ok2 = window.grab().save(expanded);
|
|
|
|
|
|
std::fprintf(stderr, "截图: 折叠态 %s / 展开态 %s\n",
|
|
|
|
|
|
ok1 ? "成功" : "失败", ok2 ? "成功" : "失败");
|
|
|
|
|
|
QCoreApplication::quit();
|
|
|
|
|
|
});
|
2026-10-12 16:06:51 +08:00
|
|
|
|
}
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 17:03:20 +08:00
|
|
|
|
if (parser.isSet("quit-after")) {
|
|
|
|
|
|
QTimer::singleShot(parser.value("quit-after").toInt() * 1000, qApp,
|
|
|
|
|
|
&QCoreApplication::quit);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-12 16:30:00 +08:00
|
|
|
|
const int rc = QCoreApplication::exec();
|
|
|
|
|
|
// 显式收尾,确保工作线程停下来之后再释放后端
|
|
|
|
|
|
live.stop();
|
|
|
|
|
|
asr.reset();
|
|
|
|
|
|
return rc;
|
2026-10-12 13:43:50 +08:00
|
|
|
|
}
|