// 命令行版:转写文件、流式转写文件、或纯命令行实时字幕(不开窗口)。 // 只依赖 core,不链接 Qt Widgets。 #include #include #include #include #include #include #include #include #include #include #include "appcli.h" #include "audio.h" #include "livepipeline.h" #include "iasr.h" #include "qwen3asr.h" #include "streamingasr.h" #include "transcript.h" namespace { std::atomic g_stop{false}; extern "C" void handleSigInt(int) { g_stop = true; } int runFile(lc::IAsr & asr, const QString & path) { std::vector pcm; std::string err; if (!lc::readWavMono(path.toStdString(), asr.sampleRate(), pcm, &err)) { std::fprintf(stderr, "读取音频失败: %s\n", err.c_str()); return 1; } const double dur = (double) pcm.size() / asr.sampleRate(); std::fprintf(stderr, "音频: %s (%.2fs)\n", qUtf8Printable(path), dur); const auto t0 = std::chrono::steady_clock::now(); lc::AsrResult r = asr.transcribe(pcm.data(), pcm.size()); const double wall = std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); if (!r.ok) { std::fprintf(stderr, "转写失败: %s\n", r.error.c_str()); return 1; } std::printf("语言: %s (%s)\n", r.transcript.language.c_str(), lc::languageDisplayName(r.transcript.language).c_str()); std::printf("文本: %s\n", r.transcript.text.c_str()); std::fprintf(stderr, "推理: %.2fs / 音频 %.2fs (rtf %.3f) 生成 %d tokens, 墙钟 %.2fs\n", r.inferSeconds, dur, dur > 0 ? r.inferSeconds / dur : 0, r.tokenCount, wall); return 0; } int runStreamFile(lc::IAsr & asr, const QString & path, const lc::StreamOptions & sop) { std::vector pcm; std::string err; if (!lc::readWavMono(path.toStdString(), asr.sampleRate(), pcm, &err)) { std::fprintf(stderr, "读取音频失败: %s\n", err.c_str()); return 1; } const double dur = (double) pcm.size() / asr.sampleRate(); std::fprintf(stderr, "流式转写: %s (%.2fs) chunk=%.1fs segment=%.1fs mode=%s\n", qUtf8Printable(path), dur, sop.chunkSec, sop.maxUtteranceSec, sop.incremental ? "incremental" : "window"); lc::StreamingAsr stream(asr, sop); const size_t chunkSamples = (size_t) (sop.chunkSec * asr.sampleRate()); const auto onStep = [](const lc::StreamingAsr::Step & s) { std::printf("\n[chunk %2d] 累计 %5.1fs | 音频 %5.2fs | 前缀 %5.2fs | 生成 %5.2fs (%d tok) " "| 合计 %5.2fs%s\n", s.chunkIndex, s.windowSeconds, s.encodeSeconds, s.prefillSeconds, s.genSeconds, s.newTokens, s.inferSeconds, s.ok ? "" : " <失败>"); if (!s.ok) std::printf(" error: %s\n", s.error.c_str()); std::printf(" 已提交: %s\n", s.committed.c_str()); std::fflush(stdout); }; for (size_t off = 0; off < pcm.size(); off += chunkSamples) { const size_t n = std::min(chunkSamples, pcm.size() - off); stream.feed(pcm.data() + off, n, onStep); } stream.flush(onStep); std::printf("\n==== 最终提交文本 (%s) ====\n%s\n", lc::languageDisplayName(stream.language()).c_str(), stream.committedText().c_str()); return 0; } int runCliLive(lc::IAsr & asr, const lc::StreamOptions & sop) { lc::LivePipelineOptions popt; popt.stream = sop; lc::LivePipeline pipeline(asr, popt); pipeline.setCaptionCallback([](const std::string & committed, const std::string & tail, const std::string & lang) { // 原地刷新:稳定部分正常显示,尾部用暗色表示未定 std::printf("\r\033[K[%s] %s\033[2m%s\033[0m", lc::languageDisplayName(lang).c_str(), committed.c_str(), tail.c_str()); std::fflush(stdout); }); pipeline.setErrorCallback([](const std::string & m) { std::fprintf(stderr, "\n采集失败: %s\n", m.c_str()); }); std::string err; if (!pipeline.start(&err)) { std::fprintf(stderr, "启动采集失败: %s\n", err.c_str()); return 1; } std::fprintf(stderr, "正在采集: %s (Ctrl+C 退出)\n", pipeline.deviceName().c_str()); std::signal(SIGINT, handleSigInt); while (!g_stop) std::this_thread::sleep_for(std::chrono::milliseconds(100)); pipeline.stop(); return 0; } } // namespace int main(int argc, char ** argv) { QCoreApplication app(argc, argv); QCommandLineParser parser; parser.setApplicationDescription(QStringLiteral("实时字幕(命令行版)")); parser.addHelpOption(); lc::app::addModelOptions(parser); lc::app::addStreamOptions(parser); lc::app::addBackendOptions(parser); parser.addOption(QCommandLineOption("file", "转写一个 wav 文件后退出", "path")); parser.addOption(QCommandLineOption("stream-file", "以流式模式转写 wav 文件后退出", "path")); parser.addOption(QCommandLineOption("cli-live", "实时转写字幕,只用命令行输出(不开窗口)")); parser.process(app); const lc::AsrOptions asrOpt = lc::app::asrOptions(parser); lc::StreamOptions sop = lc::app::streamOptions(parser); std::string err; std::unique_ptr 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; } if (parser.isSet("file")) return runFile(*asr, parser.value("file")); if (parser.isSet("stream-file")) return runStreamFile(*asr, parser.value("stream-file"), sop); return runCliLive(*asr, sop); }