2026-10-12 13:01:26 +08:00
|
|
|
|
#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; // 稳定部分
|
2026-10-12 13:32:16 +08:00
|
|
|
|
std::string tail; // 不稳定尾部(committed 之后的部分)
|
2026-10-12 13:01:26 +08:00
|
|
|
|
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);
|
2026-10-12 13:32:16 +08:00
|
|
|
|
// 处理不足一块的残留音频,并作为最后一步提交全部文本。
|
|
|
|
|
|
// 若没有待处理音频,也把当前已稳定文本提交为最终(静音分段时会用到)。
|
2026-10-12 13:01:26 +08:00
|
|
|
|
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
|