Files
live-caption/src/streamingasr.h
T
blumia 4ed4e23282 feat(ui): add floating caption window with live transcription
Add a frameless always-on-top caption window wired to the live capture
pipeline. Silence-based segmentation gates the audio so the model never
sees silence, and the panel background is painted in paintEvent with a
configurable opacity instead of a style sheet.

新增悬浮字幕窗(无边框、置顶、可折叠/展开),接入实时采集与流式转写。
分段由静音检测驱动,静音期间不送音频,避免模型对静音产生幻觉。底板
改为在 paintEvent 中绘制,不透明度可调。
2026-10-12 13:32:16 +08:00

85 lines
3.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
namespace lc {
class Qwen3Asr;
// 官方流式解码的参数(见 docs/phase2-streaming.md)
struct StreamOptions {
double chunkSec = 2.0; // 累积多少秒音频跑一步
double windowSec = 16.0; // 仅 window 模式使用:滑窗上限,<=0 表示不滑窗
int unfixedTokenNum = 5; // 前缀回滚:丢掉最后 N 个 token
int unfixedChunkNum = 2; // 前 N 个 chunk 不带前缀(冷启动)
int maxTokensPerStep = 256;
// true = 增量 KV 复用:只编码新音频,已解码音频留在 KV(推荐)
// false = 每次重编码整个窗口(Phase 2 早期的做法,仅用于对比)
bool incremental = true;
// 单段最长时长(秒):超过就提交并重置上下文(0 = 不限)。
// 真实场景应由静音检测决定分段;这里是一个兜底上界,
// 因为解码器对 KV 的注意力随总长增长,段太长单步会超预算。
double maxUtteranceSec = 30.0;
};
// 把「累积音频 → 带前缀继续生成 → 前缀回滚」这套策略封装起来。
// 输出分两部分:
// committed —— 已稳定文本(就是下一步要回喂的前缀)
// full —— 含不稳定尾部(最后 unfixedTokenNum 个 token)
class StreamingAsr {
public:
struct Step {
int chunkIndex = 0;
std::string language;
std::string committed; // 稳定部分
std::string tail; // 不稳定尾部(committed 之后的部分)
std::string full; // 含不稳定尾部
std::string rawFull; // 未 trim 的完整文本(含 language 头)
double windowSeconds = 0.0;
double encodeSeconds = 0.0;
double inferSeconds = 0.0;
int audioTokens = 0;
bool ok = true;
std::string error;
};
StreamingAsr(Qwen3Asr & asr, StreamOptions opt = {});
// 喂入 16k 单声道音频;每凑够 chunkSec 就跑一步并回调
void feed(const float * pcm, size_t n, const std::function<void(const Step &)> & onStep);
// 处理不足一块的残留音频,并作为最后一步提交全部文本。
// 若没有待处理音频,也把当前已稳定文本提交为最终(静音分段时会用到)。
void flush(const std::function<void(const Step &)> & onStep);
// 结束当前这段(例如检测到静音):清空 KV 与状态,下一段重新开始
void reset();
// 重置全部状态(含历史文本)
void resetAll();
// 全部已提交文本(含此前各段,已剥离 language 头)
std::string committedText() const;
const std::string & language() const { return language_; }
int chunkCount() const { return chunkIndex_; }
private:
void runStep(const std::vector<float> & audio, bool finalStep,
const std::function<void(const Step &)> & onStep);
Qwen3Asr & asr_;
StreamOptions opt_;
std::vector<float> buffer_; // window 模式:当前窗口音频
std::vector<float> pending_; // 尚未凑够一块的音频
double totalSeconds_ = 0.0;
std::string committedRaw_; // 当前段的原始已提交文本(含 language 头,用于回喂)
std::string historyText_; // 此前各段已提交的正文(已剥离 language 头)
std::string language_;
int chunkIndex_ = 0;
};
} // namespace lc