feat(stream): add incremental streaming decode with 8s slice reuse
Add native streaming ASR: 8s-aligned audio slices are cached in the KV cache, only new audio is encoded, and the previous text is fed back as an assistant prefix with token rollback. Text matches offline results. Streaming keeps each step within the 2s budget for segments up to 20s. 新增原生流式解码:8s 对齐的音频切片存入 KV 复用,只编码新音频,并把 上一轮文本作为 assistant 前缀回喂(带 token 回滚)。转写结果与离线 一致;20s 以内的段落每步计算都在 2s 预算内。
This commit is contained in:
1 parent
18dbc9c8d3
commit
21d68e2294
8 files changed
+678
-34
No files matched your search
@@ -0,0 +1,82 @@
|
||||
#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 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
|
||||
Reference in new issue
Block a user