Skip to content

Commit e615ed0

Browse files
authored
feat: built-in FSMN-VAD (--vad) for all three runtimes — single-binary segmentation (#2998)
Add a native ggml FSMN-VAD front end so the binaries segment long audio themselves, with no Python at runtime. New `--vad fsmn-vad.gguf` flag on llama-funasr-cli, llama-funasr-sensevoice and llama-funasr-paraformer runs FSMN-VAD (fbank + LFR + CMVN + FSMN encoder + E2EVadModel state machine, in funasr-common/funasr_vad.h) and decodes each speech segment, then concatenates. - funasr-common/funasr_vad.h: single-header VAD; segment boundaries within ~10 ms of PyTorch fsmn-vad.generate on the 184-clip set (DEFAULT_SILENCE_SCHEDULE replicated, chunk-stepped silence schedule + speech_noise_thres 0.5). - funasr-vad/: standalone VAD tool (wav -> [start_ms,end_ms] segments). - export_vad_gguf.py: export FSMN-VAD encoder + CMVN to GGUF (1.7 MB, 24 tensors). - BENCHMARKS.md: bare binary with built-in VAD reaches the reference end-to-end — full-184 micro-CER SenseVoice 8.01 / Paraformer 9.85 / Fun-ASR-Nano 8.30. Additive only: no existing code is modified outside runtime/llama.cpp/.
1 parent a83e1d9 commit e615ed0

9 files changed

Lines changed: 366 additions & 133 deletions

File tree

runtime/llama.cpp/BENCHMARKS.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ separates model-load from compute.
3838
- **Segmentation differs by system, each using its natural strategy:** FunASR uses an
3939
`fsmn-vad` front end (segments → ASR → concatenate); whisper.cpp uses its built-in
4040
30 s windowing. This is a fair system-level comparison.
41-
- **Engine-internal VAD is on the FunASR runtime roadmap.** Today the runtime reaches
42-
the reference numbers via the `fsmn-vad` front end (or `--chunk`); a native ggml VAD
43-
is planned so the bare binary hits the reference end-to-end.
44-
- **Bare binary, no VAD (whole-clip)**, for full disclosure: SenseVoiceSmall **9.99 %**,
45-
Paraformer **12.82 %** (micro, normalize_zh, full 184). Long clips decoded as one
46-
segment are out-of-distribution; this is exactly what VAD fixes.
41+
- **Engine-internal VAD is now implemented** — a native ggml FSMN-VAD built into the
42+
binaries (`--vad fsmn-vad.gguf`). The **bare binary, with no Python front end**, now
43+
reaches the reference end-to-end: SenseVoiceSmall **8.01 %**, Paraformer **9.85 %**,
44+
Fun-ASR-Nano **8.30 %** (micro, normalize_zh, full 184). The built-in C++ VAD matches
45+
the PyTorch `fsmn-vad` front end (segment boundaries within ~10 ms, slightly better
46+
CER), so the runtime is now fully self-contained.
47+
- For full disclosure, **bare binary with no VAD at all (whole-clip)** is higher —
48+
SenseVoiceSmall 9.99 %, Paraformer 12.82 % — because long clips decoded as one segment
49+
are out-of-distribution; that is exactly what the built-in VAD fixes.
4750

4851
## Why FunASR wins on Chinese
4952

runtime/llama.cpp/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ echo 'add_subdirectory(<example-dir>)' >> examples/CMakeLists.txt
3636
cmake -B build -DGGML_NATIVE=ON -DLLAMA_CURL=OFF
3737
cmake --build build -j --target <target>
3838
```
39+
The shared **FSMN-VAD** front end builds the same way (`funasr-vad/` + `funasr-common/`,
40+
target `llama-funasr-vad`); export weights with `export_vad_gguf.py`. Pass
41+
`--vad fsmn-vad.gguf` to any of the three tools for built-in long-audio segmentation.
3942

4043
## Validation
4144

@@ -44,9 +47,11 @@ SenseVoice CTC token ids identical; Paraformer text identical; Fun-ASR-Nano aggr
4447
CER matches PyTorch within 0.02% under identical conditions). See per-model READMEs.
4548

4649
## Status / notes
47-
- WAV input currently assumes 16 kHz mono PCM16.
48-
- For long audio, Fun-ASR-Nano supports `--chunk` windowing; a proper VAD front end
49-
is on the roadmap.
50+
- Any audio in (wav/mp3/flac, any rate/channels) via the bundled miniaudio loader.
51+
- **Built-in FSMN-VAD (`--vad fsmn-vad.gguf`)** segments long audio inside the binary
52+
(native ggml, no Python front end); all three tools support it. Bare-binary full-184
53+
micro-CER: SenseVoice **8.01** / Paraformer **9.85** / Fun-ASR-Nano **8.30** (see
54+
[BENCHMARKS.md](BENCHMARKS.md)). `--chunk` fixed-window remains a simpler fallback.
5055
- This adds a new `runtime/llama.cpp/` directory only; no existing code is modified.
5156

5257
## Further reading
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
"""Export FSMN-VAD (encoder + CMVN) to GGUF for the ggml C++ runtime."""
3+
import argparse, os, re
4+
import numpy as np, torch, gguf
5+
6+
def parse_mvn(path):
7+
b=[np.array([float(x) for x in m.split()],np.float32) for m in re.findall(r"\[([^\]]*)\]",open(path).read())]
8+
v=[x for x in b if x.size>1]; return v[0], v[1] # shift, scale (both 400-dim)
9+
10+
def main():
11+
ap=argparse.ArgumentParser()
12+
ap.add_argument("--model_pt",required=True); ap.add_argument("--mvn",required=True); ap.add_argument("--out",required=True)
13+
a=ap.parse_args()
14+
sd=torch.load(a.model_pt,map_location="cpu"); sd=sd.get("state_dict",sd)
15+
w=gguf.GGUFWriter(a.out,"fsmn-vad")
16+
w.add_uint32("vad.input_dim",400); w.add_uint32("vad.input_affine_dim",140)
17+
w.add_uint32("vad.linear_dim",250); w.add_uint32("vad.proj_dim",128)
18+
w.add_uint32("vad.fsmn_layers",4); w.add_uint32("vad.lorder",20)
19+
w.add_uint32("vad.output_affine_dim",140); w.add_uint32("vad.output_dim",248)
20+
w.add_uint32("vad.n_mels",80); w.add_uint32("vad.lfr_m",5); w.add_uint32("vad.lfr_n",1)
21+
shift,scale=parse_mvn(a.mvn); w.add_tensor("cmvn.shift",shift); w.add_tensor("cmvn.scale",scale)
22+
n=0
23+
for k,v in sd.items():
24+
if not k.startswith("encoder."): continue
25+
arr=v.detach().to(torch.float32).contiguous().numpy()
26+
if k.endswith("conv_left.weight"): # (C,1,lorder,1) -> (lorder,C) tap-major
27+
arr=np.ascontiguousarray(arr[:,0,:,0].T)
28+
w.add_tensor(k,arr); n+=1
29+
print(f"writing {n} tensors (+cmvn) to {a.out}")
30+
w.write_header_to_file(); w.write_kv_data_to_file(); w.write_tensors_to_file(); w.close()
31+
print(f"done: {a.out} ({os.path.getsize(a.out)/1e6:.1f} MB)")
32+
33+
if __name__=="__main__": main()

runtime/llama.cpp/fun-asr-nano/funasr-cli/funasr-cli.cpp

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
// any audio (wav/mp3/flac, any rate/channels) -> 16 kHz mono f32, via miniaudio
2727
#define FUNASR_AUDIO_IMPLEMENTATION
2828
#include "funasr_audio.h"
29+
// built-in FSMN-VAD front end (single-binary --vad segmentation)
30+
#include "funasr_vad.h"
31+
#include <utility>
2932

3033
// ======================= kaldi fbank + LFR =======================
3134
static const int FS=16000, WINLEN=400, SHIFT=160, NFFT=512, NMEL=80, LFR_M=7, LFR_N=6;
@@ -164,15 +167,18 @@ static int decode_batch(llama_context*ctx,int n,llama_token*tok,float*embd,int n
164167
}
165168

166169
int main(int argc,char**argv){
167-
std::string enc_path,llm_path,wav_path; int npred=512; double chunk_sec=0; float rep=1.0f;
170+
std::string enc_path,llm_path,wav_path,vad_path; int npred=512; double chunk_sec=0; float rep=1.0f;
171+
int vad_maxseg=30000;
168172
for(int i=1;i<argc;i++){
169173
if(!strcmp(argv[i],"--enc")&&i+1<argc)enc_path=argv[++i];
170174
else if(!strcmp(argv[i],"-m")&&i+1<argc)llm_path=argv[++i];
171175
else if(!strcmp(argv[i],"-a")&&i+1<argc)wav_path=argv[++i];
172176
else if(!strcmp(argv[i],"-n")&&i+1<argc)npred=atoi(argv[++i]);
173177
else if(!strcmp(argv[i],"--chunk")&&i+1<argc)chunk_sec=atof(argv[++i]);
178+
else if(!strcmp(argv[i],"--vad")&&i+1<argc)vad_path=argv[++i];
179+
else if(!strcmp(argv[i],"--vad-maxseg")&&i+1<argc)vad_maxseg=atoi(argv[++i]);
174180
else if(!strcmp(argv[i],"--rep")&&i+1<argc)rep=atof(argv[++i]);
175-
else {fprintf(stderr,"usage: %s --enc enc.gguf -m llm.gguf -a audio.wav [-n npred] [--chunk sec]\n",argv[0]);return 1;}
181+
else {fprintf(stderr,"usage: %s --enc enc.gguf -m llm.gguf -a audio.wav [-n npred] [--chunk sec] [--vad fsmn-vad.gguf [--vad-maxseg ms]]\n",argv[0]);return 1;}
176182
}
177183
if(enc_path.empty()||llm_path.empty()||wav_path.empty()){fprintf(stderr,"missing args\n");return 1;}
178184

@@ -199,12 +205,24 @@ int main(int argc,char**argv){
199205
std::vector<llama_token> v(n); llama_tokenize(vocab,s,strlen(s),v.data(),n,false,true); return v;};
200206
auto pre=tokenize(prefix); auto suf=tokenize(suffix);
201207

202-
// split into chunks (chunk_sec<=0 -> whole file)
203-
int chunk_n = chunk_sec > 0 ? std::max(1, (int)(chunk_sec*16000)) : (int)wav.size();
208+
// Build the list of [offset,len] windows to transcribe (in samples).
209+
// --vad : FSMN-VAD speech segments (single-binary front end, replaces fixed chunking)
210+
// --chunk sec : fixed-size chunks ; otherwise the whole file in one window
211+
std::vector<std::pair<int,int>> wins; // {sample offset, sample len}
212+
if(!vad_path.empty()){
213+
std::vector<std::pair<int,int>> segs; // ms
214+
if(!funasr_vad_segments(vad_path,wav,vad_maxseg,segs)){fprintf(stderr,"vad failed\n");return 1;}
215+
for(auto&s:segs){ int off=(int)((int64_t)s.first*16000/1000), end=(int)((int64_t)s.second*16000/1000);
216+
if(end>(int)wav.size())end=wav.size(); if(end-off>0) wins.push_back({off,end-off}); }
217+
fprintf(stderr,"[vad] %zu segments\n",wins.size());
218+
} else {
219+
int chunk_n = chunk_sec > 0 ? std::max(1, (int)(chunk_sec*16000)) : (int)wav.size();
220+
for(size_t off=0; off<wav.size(); off+=chunk_n) wins.push_back({(int)off,(int)std::min((size_t)chunk_n,wav.size()-off)});
221+
}
204222
std::string full;
205-
for (size_t off = 0; off < wav.size(); off += chunk_n) {
206-
int len = std::min((size_t)chunk_n, wav.size()-off);
207-
if (len < WINLEN) break; // too short for one frame
223+
for (auto& w : wins) {
224+
int off = w.first, len = w.second;
225+
if (len < WINLEN) continue; // too short for one frame
208226
std::vector<float> seg(wav.begin()+off, wav.begin()+off+len);
209227
int T=0; auto fbank=compute_fbank(seg,T);
210228
int D=0; auto adp=run_encoder(em,fbank,T,560,D);
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// funasr_vad.h — single-header FSMN-VAD for the funasr ggml runtime.
2+
// Exposes funasr_vad_segments(): 16k mono wav -> speech segments [start_ms,end_ms].
3+
// Front end (80-mel fbank + LFR m5n1 + CMVN) and FSMN encoder validated bit-exact vs
4+
// PyTorch fsmn-vad; the host state machine reproduces E2EVadModel (DEFAULT_SILENCE_SCHEDULE,
5+
// chunk-stepped) to within 1 frame (10ms) of fsmn-vad.generate on the 184-clip set.
6+
#pragma once
7+
#include "ggml.h"
8+
#include "ggml-cpu.h"
9+
#include "ggml-alloc.h"
10+
#include "ggml-backend.h"
11+
#include "gguf.h"
12+
#include <cmath>
13+
#include <cstdint>
14+
#include <cstdio>
15+
#include <cstring>
16+
#include <map>
17+
#include <string>
18+
#include <utility>
19+
#include <vector>
20+
21+
namespace funasr_vad_impl {
22+
static const int FS=16000,WINLEN=400,SHIFT=160,NFFT=512,NMEL=80;
23+
static const float PREEMPH=0.97f,LOWF=20.0f,HIGHF=8000.0f;
24+
static inline float melf(float f){return 1127.0f*logf(1.0f+f/700.0f);}
25+
static void fftc(std::vector<float>&re,std::vector<float>&im,int n){for(int i=1,j=0;i<n;i++){int b=n>>1;for(;j&b;b>>=1)j^=b;j^=b;if(i<j){std::swap(re[i],re[j]);std::swap(im[i],im[j]);}}
26+
for(int len=2;len<=n;len<<=1){double a=-2.0*M_PI/len;float wr=cosf(a),wi=sinf(a);for(int i=0;i<n;i+=len){float cr=1,ci=0;for(int k=0;k<len/2;k++){float ur=re[i+k],ui=im[i+k];
27+
float vr=re[i+k+len/2]*cr-im[i+k+len/2]*ci,vi=re[i+k+len/2]*ci+im[i+k+len/2]*cr;re[i+k]=ur+vr;im[i+k]=ui+vi;re[i+k+len/2]=ur-vr;im[i+k+len/2]=ui-vi;float nc=cr*wr-ci*wi;ci=cr*wi+ci*wr;cr=nc;}}}}
28+
static std::vector<std::vector<float>> fbank80(std::vector<float> wav){
29+
for(auto&v:wav)v*=32768.0f; std::vector<float>win(WINLEN);
30+
for(int i=0;i<WINLEN;i++)win[i]=0.54f-0.46f*cosf(2.0f*M_PI*i/(WINLEN-1));
31+
const int NB=NFFT/2+1; float bw=(float)FS/NFFT,ml=melf(LOWF),mh=melf(HIGHF),dm=(mh-ml)/(NMEL+1);
32+
std::vector<std::vector<float>>fb(NMEL,std::vector<float>(NB,0.0f));
33+
for(int m=0;m<NMEL;m++){float L=ml+m*dm,C=ml+(m+1)*dm,R=ml+(m+2)*dm;for(int k=0;k<NB;k++){float mf=melf(bw*k);if(mf>L&&mf<R)fb[m][k]=mf<=C?(mf-L)/(C-L):(R-mf)/(R-C);}}
34+
int N=wav.size(),T=(N-WINLEN)/SHIFT+1; if(T<1)T=0; std::vector<std::vector<float>>feat(T,std::vector<float>(NMEL));
35+
std::vector<float>re(NFFT),im(NFFT),fr(WINLEN);const float fl=1.1920929e-07f;
36+
for(int t=0;t<T;t++){const float*s=wav.data()+t*SHIFT;double mn=0;for(int i=0;i<WINLEN;i++)mn+=s[i];mn/=WINLEN;
37+
for(int i=0;i<WINLEN;i++)fr[i]=s[i]-(float)mn;for(int i=WINLEN-1;i>0;i--)fr[i]-=PREEMPH*fr[i-1];fr[0]-=PREEMPH*fr[0];
38+
for(int i=0;i<NFFT;i++){re[i]=i<WINLEN?fr[i]*win[i]:0.0f;im[i]=0.0f;}fftc(re,im,NFFT);
39+
for(int m=0;m<NMEL;m++){float e=0;for(int k=0;k<NB;k++)if(fb[m][k]>0)e+=fb[m][k]*(re[k]*re[k]+im[k]*im[k]);feat[t][m]=logf(e>fl?e:fl);}}
40+
return feat;
41+
}
42+
static std::vector<float> lfr(const std::vector<std::vector<float>>&feat,int m,int n,int&T_out){
43+
int T=feat.size(),D=NMEL,pad=(m-1)/2; int Tl=T>0?(T+n-1)/n:0;
44+
std::vector<std::vector<float>> pf; pf.reserve(T+pad+m);
45+
for(int i=0;i<pad;i++)pf.push_back(feat[0]);
46+
for(int t=0;t<T;t++)pf.push_back(feat[t]);
47+
while((int)pf.size()<(Tl-1)*n+m)pf.push_back(feat[T-1]);
48+
std::vector<float> out((size_t)Tl*m*D);
49+
for(int i=0;i<Tl;i++)for(int j=0;j<m;j++)memcpy(&out[((size_t)i*m+j)*D],pf[i*n+j].data(),D*sizeof(float));
50+
T_out=Tl; return out;
51+
}
52+
struct vad{ggml_context*ctx=nullptr;std::map<std::string,ggml_tensor*>t;
53+
ggml_tensor*g(const std::string&n){auto it=t.find(n);if(it==t.end()){fprintf(stderr,"vad: missing %s\n",n.c_str());return nullptr;}return it->second;}};
54+
static ggml_tensor* lin(ggml_context*c,ggml_tensor*w,ggml_tensor*b,ggml_tensor*x){auto y=ggml_mul_mat(c,w,x);return b?ggml_add(c,y,b):y;}
55+
} // namespace funasr_vad_impl
56+
57+
// Run FSMN-VAD on a 16k mono float waveform; fills segs with [start_ms,end_ms] speech spans.
58+
// max_seg_ms caps a single segment (e.g. 30000); pass <=0 to use 60000 (model default).
59+
inline bool funasr_vad_segments(const std::string& gguf_path, const std::vector<float>& wav,
60+
int max_seg_ms, std::vector<std::pair<int,int>>& segs, int nthreads=8){
61+
using namespace funasr_vad_impl;
62+
segs.clear();
63+
vad m; gguf_init_params ip={false,&m.ctx}; gguf_context*gg=gguf_init_from_file(gguf_path.c_str(),ip);
64+
if(!gg){fprintf(stderr,"vad: cannot load %s\n",gguf_path.c_str());return false;}
65+
auto rd=[&](const char*k,int d){int i=gguf_find_key(gg,k);return i<0?d:(int)gguf_get_val_u32(gg,i);};
66+
int idim=rd("vad.input_dim",400),pd=rd("vad.proj_dim",128),nl=rd("vad.fsmn_layers",4),lorder=rd("vad.lorder",20),
67+
od=rd("vad.output_dim",248),lm=rd("vad.lfr_m",5),ln=rd("vad.lfr_n",1);
68+
for(int i=0;i<gguf_get_n_tensors(gg);i++){const char*nm=gguf_get_tensor_name(gg,i);m.t[nm]=ggml_get_tensor(m.ctx,nm);}
69+
gguf_free(gg);
70+
71+
auto feat=fbank80(wav); int T=0; auto feats=lfr(feat,lm,ln,T); // [T,400]
72+
if(T<1){if(m.ctx)ggml_free(m.ctx);return true;} // too short -> no speech
73+
float*shift=(float*)m.g("cmvn.shift")->data,*scale=(float*)m.g("cmvn.scale")->data;
74+
for(int t=0;t<T;t++)for(int d=0;d<idim;d++)feats[(size_t)t*idim+d]=(feats[(size_t)t*idim+d]+shift[d])*scale[d];
75+
76+
ggml_backend_t be=ggml_backend_cpu_init();
77+
ggml_init_params cp={(size_t)512*1024*1024,nullptr,true}; ggml_context*c=ggml_init(cp);
78+
ggml_tensor*x=ggml_new_tensor_2d(c,GGML_TYPE_F32,idim,T); ggml_set_input(x);
79+
ggml_tensor*h=lin(c,m.g("encoder.in_linear1.linear.weight"),m.g("encoder.in_linear1.linear.bias"),x);
80+
h=lin(c,m.g("encoder.in_linear2.linear.weight"),m.g("encoder.in_linear2.linear.bias"),h); h=ggml_relu(c,h);
81+
for(int i=0;i<nl;i++){std::string p="encoder.fsmn."+std::to_string(i)+".";
82+
ggml_tensor*z=ggml_mul_mat(c,m.g(p+"linear.linear.weight"),h);
83+
ggml_tensor*fk=m.g(p+"fsmn_block.conv_left.weight"); ggml_tensor*zp=ggml_pad_ext(c,z,0,0,lorder-1,0,0,0,0,0); ggml_tensor*acc=z;
84+
for(int j=0;j<lorder;j++){auto sl=ggml_view_2d(c,zp,pd,T,zp->nb[1],(size_t)j*zp->nb[1]);auto wj=ggml_view_1d(c,fk,pd,(size_t)j*fk->nb[1]);acc=ggml_add(c,acc,ggml_mul(c,ggml_cont(c,sl),wj));}
85+
ggml_tensor*a=lin(c,m.g(p+"affine.linear.weight"),m.g(p+"affine.linear.bias"),acc); h=ggml_relu(c,a);}
86+
h=lin(c,m.g("encoder.out_linear1.linear.weight"),m.g("encoder.out_linear1.linear.bias"),h);
87+
h=lin(c,m.g("encoder.out_linear2.linear.weight"),m.g("encoder.out_linear2.linear.bias"),h);
88+
h=ggml_soft_max(c,h); ggml_set_output(h);
89+
ggml_cgraph*gf=ggml_new_graph(c); ggml_build_forward_expand(gf,h);
90+
ggml_gallocr_t ga=ggml_gallocr_new(ggml_backend_cpu_buffer_type()); ggml_gallocr_alloc_graph(ga,gf);
91+
ggml_backend_tensor_set(x,feats.data(),0,ggml_nbytes(x)); ggml_backend_cpu_set_n_threads(be,nthreads);
92+
bool ok=ggml_backend_graph_compute(be,gf)==GGML_STATUS_SUCCESS;
93+
std::vector<float> sc((size_t)od*T); if(ok)ggml_backend_tensor_get(h,sc.data(),0,ggml_nbytes(h));
94+
ggml_gallocr_free(ga);ggml_free(c);ggml_backend_free(be);if(m.ctx)ggml_free(m.ctx);
95+
if(!ok)return false;
96+
97+
// ===== E2EVadModel state machine (host) -> speech segments [start_ms,end_ms] =====
98+
const int FR=10; // ms per frame (frame_in_ms)
99+
const int win=20; // window_size_ms 200 / 10
100+
const int s2s=15, sp2s=15; // sil_to_speech / speech_to_sil thres (150/10)
101+
const int lookahead_end=100/FR; // lookahead_time_end_point 100/10 = 10 (do_extend)
102+
int max_seg = (max_seg_ms>0 ? max_seg_ms : 60000)/FR; // max_single_segment frames
103+
const int start_lookback = win + 200/FR; // LatencyFrmNumAtStartPoint = 40
104+
// End-silence threshold from DEFAULT_SILENCE_SCHEDULE, stepped at chunk boundaries (chunk_size
105+
// 60000ms = 6000 frames). At each boundary, if mid-segment (InSpeech) or in_speech latched,
106+
// accumulated_ms += 60000; threshold = schedule(accumulated). Reset to 0 on each segment emit.
107+
const int CHUNK=60000/FR;
108+
int acc=0, insp=0; int max_end_sil, end_lookback;
109+
auto recompute=[&](){
110+
int s; if(acc<=10000)s=2000; else if(acc<=20000)s=1000; else if(acc<=30000)s=800;
111+
else if(acc<=40000)s=600; else if(acc<=50000)s=400; else if(acc<=60000)s=200; else s=100;
112+
int ms=s-150; if(ms<0)ms=0; max_end_sil=ms/FR;
113+
end_lookback=max_end_sil-lookahead_end-1; if(end_lookback<0)end_lookback=0;
114+
};
115+
recompute();
116+
std::vector<int> wbuf(win,0); int wpos=0,wsum=0,pre=0;
117+
int st=0, cstart=-1, csil=0, prev_end=0;
118+
auto reset=[&](){ std::fill(wbuf.begin(),wbuf.end(),0); wpos=0; wsum=0; pre=0; csil=0; st=0; cstart=-1; acc=0; insp=0; };
119+
auto emit=[&](int s,int e){ if(s<prev_end)s=prev_end; if(s<0)s=0; if(e>T)e=T; if(e>s){segs.push_back({s,e}); prev_end=e;} };
120+
for(int t=0;t<T;t++){
121+
if(t>0 && t%CHUNK==0){ if(st==1||insp){acc+=60000; insp=1;} recompute(); }
122+
float sil=sc[(size_t)t*od+0];
123+
int fs = ((1.0f-sil) >= sil + 0.5f) ? 1 : 0; // speech_noise_thres=0.5
124+
wsum -= wbuf[wpos]; wsum += fs; wbuf[wpos]=fs; wpos=(wpos+1)%win;
125+
int ch;
126+
if(pre==0 && wsum>=s2s){pre=1; ch=3;}
127+
else if(pre==1 && wsum<=sp2s){pre=0; ch=1;}
128+
else ch = pre==0?0:2;
129+
if(ch==3){ csil=0;
130+
if(st==0){ cstart=t-start_lookback; if(cstart<prev_end)cstart=prev_end; if(cstart<0)cstart=0; st=1; }
131+
else if(st==1 && t-cstart+1>max_seg){ emit(cstart,t); reset(); }
132+
} else if(ch==1||ch==2){ csil=0;
133+
if(st==1 && t-cstart+1>max_seg){ emit(cstart,t); reset(); }
134+
} else { csil++;
135+
if(st==1){
136+
if(csil>=max_end_sil){ emit(cstart, t-end_lookback); reset(); }
137+
else if(t-cstart+1>max_seg){ emit(cstart,t); reset(); }
138+
}
139+
}
140+
}
141+
if(st==1) emit(cstart,T);
142+
// convert frame indices -> ms
143+
for(auto&s:segs){ s.first*=FR; s.second*=FR; }
144+
return true;
145+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
set(TARGET llama-funasr-vad)
2+
add_executable(${TARGET} funasr-vad.cpp)
3+
install(TARGETS ${TARGET} RUNTIME)
4+
target_include_directories(${TARGET} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../funasr-common)
5+
target_link_libraries(${TARGET} PRIVATE ggml ${CMAKE_THREAD_LIBS_INIT})
6+
target_compile_features(${TARGET} PRIVATE cxx_std_17)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// funasr-vad: FSMN-VAD on ggml. WAV(any fmt/rate) -> speech segments [start_ms,end_ms].
2+
// Front end + FSMN encoder validated bit-exact vs PyTorch; state machine reproduces
3+
// E2EVadModel segmentation to within 1 frame (10ms) of fsmn-vad.generate on the 184-clip set.
4+
#define FUNASR_AUDIO_IMPLEMENTATION
5+
#include "funasr_audio.h"
6+
#include "funasr_vad.h"
7+
#include <cstdio>
8+
#include <cstdlib>
9+
#include <cstring>
10+
#include <string>
11+
#include <utility>
12+
#include <vector>
13+
14+
int main(int argc,char**argv){
15+
std::string gp,wp;
16+
for(int i=1;i<argc;i++){if(!strcmp(argv[i],"-m")&&i+1<argc)gp=argv[++i];else if(!strcmp(argv[i],"-a")&&i+1<argc)wp=argv[++i];}
17+
if(gp.empty()||wp.empty()){fprintf(stderr,"usage: %s -m fsmn-vad.gguf -a audio.wav\n",argv[0]);return 1;}
18+
std::vector<float> wav; if(!funasr_load_audio_16k_mono(wp.c_str(),wav)){fprintf(stderr,"read audio failed\n");return 1;}
19+
int max_seg = getenv("VAD_MAXSEG") ? atoi(getenv("VAD_MAXSEG")) : 30000;
20+
std::vector<std::pair<int,int>> segs;
21+
if(!funasr_vad_segments(gp,wav,max_seg,segs)){fprintf(stderr,"vad failed\n");return 1;}
22+
for(auto&s:segs) printf("%d %d\n", s.first, s.second);
23+
fprintf(stderr,"[vad] %zu segments (max_seg=%dms)\n",segs.size(),max_seg);
24+
return 0;
25+
}

0 commit comments

Comments
 (0)