2026-10-12 13:43:50 +08:00
|
|
|
|
// 图形界面版:悬浮字幕窗。
|
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
|
|
#include <QApplication>
|
|
|
|
|
|
#include <QCommandLineOption>
|
|
|
|
|
|
#include <QCommandLineParser>
|
|
|
|
|
|
#include <QGuiApplication>
|
|
|
|
|
|
#include <QTimer>
|
|
|
|
|
|
|
|
|
|
|
|
#include "appcli.h"
|
|
|
|
|
|
#include "captionwindow.h"
|
|
|
|
|
|
#include "livetranscriber.h"
|
2026-10-12 16:06:51 +08:00
|
|
|
|
#include "iasr.h"
|
2026-10-12 13:43:50 +08:00
|
|
|
|
#include "qwen3asr.h"
|
|
|
|
|
|
#include "streamingasr.h"
|
|
|
|
|
|
#include "transcript.h"
|
|
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
2026-10-12 16:06:51 +08:00
|
|
|
|
int runGui(lc::IAsr & asr, const lc::StreamOptions & sop,
|
2026-10-12 13:43:50 +08:00
|
|
|
|
const QString & screenshotPath, int screenshotDelaySec, int opacityPercent)
|
|
|
|
|
|
{
|
|
|
|
|
|
lc::CaptionWindow window;
|
|
|
|
|
|
window.setPanelOpacity(opacityPercent);
|
|
|
|
|
|
lc::LiveTranscriber live(asr);
|
|
|
|
|
|
|
|
|
|
|
|
QObject::connect(&live, &lc::LiveTranscriber::caption, &window,
|
|
|
|
|
|
[&window](const QString & committed, const QString & tail,
|
|
|
|
|
|
const QString & lang) {
|
|
|
|
|
|
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));
|
|
|
|
|
|
});
|
|
|
|
|
|
QObject::connect(&window, &lc::CaptionWindow::closed, qApp, &QCoreApplication::quit);
|
|
|
|
|
|
QObject::connect(&window, &lc::CaptionWindow::languageClicked, &window, [] {
|
|
|
|
|
|
qInfo() << "语言标签被点击(设置界面将在 Phase 4 接入)";
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
QString err;
|
|
|
|
|
|
if (!live.start(sop, &err)) {
|
|
|
|
|
|
qCritical("启动采集失败: %s", qUtf8Printable(err));
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
qInfo("正在采集: %s", qUtf8Printable(live.deviceName()));
|
|
|
|
|
|
|
|
|
|
|
|
window.show();
|
|
|
|
|
|
|
|
|
|
|
|
if (!screenshotPath.isEmpty()) {
|
|
|
|
|
|
// 自截图:用于验证渲染(不依赖外部截图工具)
|
|
|
|
|
|
QTimer::singleShot(screenshotDelaySec * 1000, &window, [&window, screenshotPath] {
|
|
|
|
|
|
const bool ok1 = window.grab().save(screenshotPath);
|
|
|
|
|
|
|
|
|
|
|
|
window.setCollapsed(false);
|
|
|
|
|
|
QString expandedPath = screenshotPath;
|
|
|
|
|
|
expandedPath.replace(QStringLiteral(".png"), QStringLiteral("-expanded.png"));
|
|
|
|
|
|
const bool ok2 = window.grab().save(expandedPath);
|
|
|
|
|
|
|
|
|
|
|
|
std::fprintf(stderr, "截图: 折叠态 %s / 展开态 %s\n",
|
|
|
|
|
|
ok1 ? "成功" : "失败", ok2 ? "成功" : "失败");
|
|
|
|
|
|
QCoreApplication::quit();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return QCoreApplication::exec();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
|
|
int main(int argc, char ** argv)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 说明意图:DPI 取整策略必须在 QGuiApplication 之前定。
|
|
|
|
|
|
// (Deepin 的会话环境已经设了 QT_SCALE_FACTOR_ROUNDING_POLICY,
|
|
|
|
|
|
// 所以本机仍会看到 Qt 的那条提醒,那不是本程序的问题。)
|
|
|
|
|
|
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
|
|
|
|
|
|
Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
|
|
|
|
|
|
|
|
|
|
|
QApplication app(argc, argv);
|
|
|
|
|
|
QCoreApplication::setApplicationName(QStringLiteral("live-caption"));
|
|
|
|
|
|
|
|
|
|
|
|
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 13:54:04 +08:00
|
|
|
|
parser.addOption(lc::app::makeOption("opacity", "字幕背景不透明度,取值 20-100", "n", "84"));
|
|
|
|
|
|
parser.addOption(lc::app::makeOption("screenshot", "启动后自动截图并退出(验证渲染用)", "path"));
|
|
|
|
|
|
parser.addOption(lc::app::makeOption("screenshot-delay", "截图前等待秒数", "sec", "6"));
|
2026-10-12 13:43:50 +08:00
|
|
|
|
parser.process(app);
|
|
|
|
|
|
|
|
|
|
|
|
const lc::AsrOptions asrOpt = lc::app::asrOptions(parser);
|
2026-10-12 16:06:51 +08:00
|
|
|
|
lc::StreamOptions sop = lc::app::streamOptions(parser);
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 16:06:51 +08:00
|
|
|
|
std::string err;
|
|
|
|
|
|
std::unique_ptr<lc::IAsr> asr = lc::app::makeAsr(parser, asrOpt, &err);
|
|
|
|
|
|
if (!asr) {
|
|
|
|
|
|
std::fprintf(stderr, "%s\n", err.c_str());
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (asr->kind() == lc::AsrKind::NativeStreaming && !parser.isSet("stream-chunk")) {
|
|
|
|
|
|
sop.chunkSec = 0.32;
|
|
|
|
|
|
}
|
2026-10-12 13:43:50 +08:00
|
|
|
|
|
2026-10-12 16:06:51 +08:00
|
|
|
|
return runGui(*asr, sop,
|
2026-10-12 13:43:50 +08:00
|
|
|
|
parser.value("screenshot"),
|
|
|
|
|
|
parser.value("screenshot-delay").toInt(),
|
|
|
|
|
|
parser.value("opacity").toInt());
|
|
|
|
|
|
}
|