Add an append-only streaming path that feeds each audio chunk as a new conversation turn, so nothing is ever recomputed: the KV grows by append, no prefix is re-decoded and no audio is re-encoded. Per-step cost becomes flat instead of growing with the segment (RTF 1.06 -> 0.26 on 62.6s of continuous speech), but accuracy requires ~8s chunks because the audio encoder needs its 8s attention window, so text only appears every 8s. The incremental mode stays the default. 新增追加式流式:每块音频作为一个新的对话轮追加,前缀不重算、旧音频不重 编码,单步成本不再随段落增长(连续 62.6s 语音 RTF 从 1.06 降到 0.26)。 但准确率要求 chunk≈8s,否则会被截断,所以出字粒度是 8s;增量式仍为默认。
91 lines
3.5 KiB
C++
91 lines
3.5 KiB
C++
#pragma once
|
||
|
||
#include <cstdio>
|
||
#include <string>
|
||
|
||
#include <QCommandLineOption>
|
||
#include <QCommandLineParser>
|
||
|
||
#include "qwen3asr.h"
|
||
#include "streamingasr.h"
|
||
|
||
// 两个可执行程序(GUI / CLI)共用的命令行选项定义与解析。
|
||
namespace lc::app {
|
||
|
||
// 构造命令行选项。
|
||
//
|
||
// Qt 自己不会把默认值印进 --help(构造函数里传的默认值只作为 value() 的兜底),
|
||
// 所以这里在给了默认值时把「(默认 X)」拼到说明后面。
|
||
inline QCommandLineOption makeOption(const QString & name, const QString & description,
|
||
const QString & valueName = QString(),
|
||
const QString & defaultValue = QString())
|
||
{
|
||
QString desc = description;
|
||
if (!defaultValue.isEmpty()) {
|
||
desc += QStringLiteral("(默认 %1)").arg(defaultValue);
|
||
}
|
||
if (valueName.isEmpty()) return QCommandLineOption(name, desc);
|
||
return QCommandLineOption(name, desc, valueName, defaultValue);
|
||
}
|
||
|
||
inline void addModelOptions(QCommandLineParser & p)
|
||
{
|
||
p.addOption(makeOption("model", "主模型 GGUF 路径(架构须为 qwen3vl)", "path"));
|
||
p.addOption(makeOption("mmproj", "mmproj GGUF 路径(clip + qwen3a)", "path"));
|
||
p.addOption(makeOption("threads", "推理线程数", "n", "8"));
|
||
p.addOption(makeOption("max-tokens", "单段最多生成 token 数", "n", "256"));
|
||
p.addOption(makeOption("verbose", "输出 llama.cpp/mtmd 的详细日志"));
|
||
}
|
||
|
||
inline void addStreamOptions(QCommandLineParser & p)
|
||
{
|
||
p.addOption(makeOption("stream-chunk", "流式 chunk / 提交粒度,单位秒", "sec", "2.0"));
|
||
p.addOption(makeOption("stream-window", "滑窗上限,单位秒;仅 window 模式,0=不滑窗", "sec", "16"));
|
||
p.addOption(makeOption("stream-segment", "单段最长时长,单位秒;0=不限", "sec", "30"));
|
||
p.addOption(makeOption("unfixed-tokens", "前缀回滚丢弃的 token 数", "n", "5"));
|
||
p.addOption(makeOption("unfixed-chunks", "冷启动不带前缀的 chunk 数", "n", "2"));
|
||
p.addOption(makeOption("stream-mode", "流式后端: incremental / append / window", "mode",
|
||
"incremental"));
|
||
}
|
||
|
||
inline AsrOptions asrOptions(const QCommandLineParser & p)
|
||
{
|
||
AsrOptions o;
|
||
o.modelPath = p.value("model").toStdString();
|
||
o.mmprojPath = p.value("mmproj").toStdString();
|
||
o.threads = p.value("threads").toInt();
|
||
o.maxTokens = p.value("max-tokens").toInt();
|
||
o.verbose = p.isSet("verbose");
|
||
return o;
|
||
}
|
||
|
||
inline StreamOptions streamOptions(const QCommandLineParser & p)
|
||
{
|
||
StreamOptions o;
|
||
o.chunkSec = p.value("stream-chunk").toDouble();
|
||
o.windowSec = p.value("stream-window").toDouble();
|
||
o.maxUtteranceSec = p.value("stream-segment").toDouble();
|
||
o.unfixedTokenNum = p.value("unfixed-tokens").toInt();
|
||
o.unfixedChunkNum = p.value("unfixed-chunks").toInt();
|
||
o.maxTokensPerStep = p.value("max-tokens").toInt();
|
||
o.incremental = p.value("stream-mode") == QLatin1String("incremental");
|
||
o.appendOnly = p.value("stream-mode") == QLatin1String("append");
|
||
return o;
|
||
}
|
||
|
||
inline bool loadModel(Qwen3Asr & asr, const AsrOptions & opt)
|
||
{
|
||
if (opt.modelPath.empty() || opt.mmprojPath.empty()) {
|
||
std::fprintf(stderr, "必须提供 --model 和 --mmproj\n");
|
||
return false;
|
||
}
|
||
std::string err;
|
||
if (!asr.load(opt, &err)) {
|
||
std::fprintf(stderr, "加载失败: %s\n", err.c_str());
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
} // namespace lc::app
|