// funasr-cli: end-to-end Fun-ASR-Nano in C++ on the llama.cpp / ggml stack. // // wav(16k mono) -> kaldi fbank -> SAN-M encoder + adaptor (ggml) -> // low-frame-rate truncation -> [prefix tokens | audio embeds | suffix tokens] // -> Qwen3 LLM (llama.cpp) -> transcription. // // This is the whisper.cpp-style single-binary path: no Python at runtime. // // funasr-cli --enc funasr-encoder.gguf -m qwen3-0.6b.gguf -a audio.wav #include "ggml.h" #include "ggml-cpu.h" #include "ggml-alloc.h" #include "ggml-backend.h" #include "gguf.h" #include "llama.h" #include #include #include #include #include #include #include // any audio (wav/mp3/flac, any rate/channels) -> 16 kHz mono f32, via miniaudio #define FUNASR_AUDIO_IMPLEMENTATION #include "funasr_audio.h" // built-in FSMN-VAD front end (single-binary --vad segmentation) #include "funasr_vad.h" #include // ======================= kaldi fbank + LFR ======================= static const int FS=16000, WINLEN=400, SHIFT=160, NFFT=512, NMEL=80, LFR_M=7, LFR_N=6; static const float PREEMPH=0.97f, LOWF=20.0f, HIGHF=8000.0f; static inline float mel(float f){ return 1127.0f*logf(1.0f+f/700.0f); } static void fft(std::vector&re,std::vector&im,int n){ for(int i=1,j=0;i>1;for(;j&b;b>>=1)j^=b;j^=b;if(i compute_fbank(std::vector wav, int & T_out) { for (auto & v : wav) v *= 32768.0f; std::vector win(WINLEN); for (int i=0;i> fb(NMEL, std::vector(NBIN,0.0f)); for(int m=0;mL&&mf> feat(T, std::vector(NMEL)); std::vector re(NFFT),im(NFFT),fr(WINLEN); const float fl=1.1920929e-07f; for(int t=0;t0;i--)fr[i]-=PREEMPH*fr[i-1];fr[0]-=PREEMPH*fr[0]; for(int i=0;i0)e+=fb[m][k]*(re[k]*re[k]+im[k]*im[k]); feat[t][m]=logf(e>fl?e:fl);}} // LFR const int pad=(LFR_M-1)/2; int T_lfr=(T+LFR_N-1)/LFR_N; std::vector> pd; pd.reserve(T+pad+LFR_M); for(int i=0;i out((size_t)T_lfr*D); for(int i=0;i t; ggml_tensor* g(const std::string&n){auto it=t.find(n);if(it==t.end()){fprintf(stderr,"missing %s\n",n.c_str());exit(1);}return it->second;} }; static const float LN_EPS=1e-5f; static bool load_enc(const char*p, enc_model&m){ gguf_init_params gp={false,&m.ctx_w}; gguf_context*g=gguf_init_from_file(p,gp); if(!g)return false; auto rd=[&](const char*k,int d){int i=gguf_find_key(g,k);return i<0?d:(int)gguf_get_val_u32(g,i);}; m.c.d_model=rd("funasr.enc.output_size",512); m.c.n_head=rd("funasr.enc.attention_heads",4); m.c.num_blocks=rd("funasr.enc.num_blocks",50); m.c.tp_blocks=rd("funasr.enc.tp_blocks",20); m.c.kernel=rd("funasr.enc.kernel_size",11); m.c.adp_llm=rd("funasr.adp.llm_dim",1024); m.c.adp_layers=rd("funasr.adp.n_layer",2); m.c.adp_head=rd("funasr.adp.attention_heads",8); int n=gguf_get_n_tensors(g); for(int i=0;inb[1]; ggml_tensor*q=ggml_cont(c,ggml_view_2d(c,qkv,D,T,nb1,0)); ggml_tensor*k=ggml_cont(c,ggml_view_2d(c,qkv,D,T,nb1,(size_t)D*sizeof(float))); ggml_tensor*v=ggml_cont(c,ggml_view_2d(c,qkv,D,T,nb1,(size_t)2*D*sizeof(float))); const int pad=(K-1)/2; ggml_tensor*fk=m.g(p+"fsmn_block.weight"); ggml_tensor*vp=ggml_pad_ext(c,v,0,0,pad,pad,0,0,0,0); ggml_tensor*fsmn=v; for(int j=0;jnb[1],(size_t)j*vp->nb[1]); auto wj=ggml_view_1d(c,fk,D,(size_t)j*fk->nb[1]); fsmn=ggml_add(c,fsmn,ggml_mul(c,ggml_cont(c,sl),wj));} q=ggml_permute(c,ggml_reshape_3d(c,q,dk,H,T),0,2,1,3); k=ggml_permute(c,ggml_reshape_3d(c,k,dk,H,T),0,2,1,3); ggml_tensor*vh=ggml_cont(c,ggml_permute(c,ggml_reshape_3d(c,v,dk,H,T),1,2,0,3)); ggml_tensor*kq=ggml_soft_max(c,ggml_scale(c,ggml_mul_mat(c,k,q),1.0f/sqrtf((float)dk))); ggml_tensor*o=ggml_cont_2d(c,ggml_permute(c,ggml_mul_mat(c,vh,kq),0,2,1,3),D,T); return ggml_add(c,lin(c,m.g(p+"linear_out.weight"),m.g(p+"linear_out.bias"),o),fsmn); } static ggml_tensor* sanm_layer(ggml_context*c,enc_model&m,const std::string&p,ggml_tensor*x,int T,bool res){ auto r=x; auto h=lnorm(c,x,m.g(p+"norm1.weight"),m.g(p+"norm1.bias")); auto sa=sanm_attn(c,m,p+"self_attn.",h,T); x=res?ggml_add(c,r,sa):sa; r=x; h=lnorm(c,x,m.g(p+"norm2.weight"),m.g(p+"norm2.bias")); h=lin(c,m.g(p+"feed_forward.w_1.weight"),m.g(p+"feed_forward.w_1.bias"),h); h=ggml_relu(c,h); h=lin(c,m.g(p+"feed_forward.w_2.weight"),m.g(p+"feed_forward.w_2.bias"),h); return ggml_add(c,r,h); } static ggml_tensor* adp_layer(ggml_context*c,enc_model&m,const std::string&p,ggml_tensor*x,int T){ const int D=m.c.adp_llm,H=m.c.adp_head,dk=D/H; auto r=x; auto h=lnorm(c,x,m.g(p+"norm1.weight"),m.g(p+"norm1.bias")); auto q=ggml_permute(c,ggml_reshape_3d(c,lin(c,m.g(p+"self_attn.linear_q.weight"),m.g(p+"self_attn.linear_q.bias"),h),dk,H,T),0,2,1,3); auto k=ggml_permute(c,ggml_reshape_3d(c,lin(c,m.g(p+"self_attn.linear_k.weight"),m.g(p+"self_attn.linear_k.bias"),h),dk,H,T),0,2,1,3); auto vh=ggml_cont(c,ggml_permute(c,ggml_reshape_3d(c,lin(c,m.g(p+"self_attn.linear_v.weight"),m.g(p+"self_attn.linear_v.bias"),h),dk,H,T),1,2,0,3)); auto kq=ggml_soft_max(c,ggml_scale(c,ggml_mul_mat(c,k,q),1.0f/sqrtf((float)dk))); auto o=ggml_cont_2d(c,ggml_permute(c,ggml_mul_mat(c,vh,kq),0,2,1,3),D,T); x=ggml_add(c,r,lin(c,m.g(p+"self_attn.linear_out.weight"),m.g(p+"self_attn.linear_out.bias"),o)); r=x; h=lnorm(c,x,m.g(p+"norm2.weight"),m.g(p+"norm2.bias")); h=lin(c,m.g(p+"feed_forward.w_1.weight"),m.g(p+"feed_forward.w_1.bias"),h); h=ggml_relu(c,h); h=lin(c,m.g(p+"feed_forward.w_2.weight"),m.g(p+"feed_forward.w_2.bias"),h); return ggml_add(c,r,h); } static void add_posenc(std::vector&x,int T,int depth){ double inc=log(10000.0)/(depth/2.0-1.0); for(int t=0;t adaptor out [T x adp_llm] row-major static std::vector run_encoder(enc_model&m,std::vector fbank,int T,int F,int&Dout){ float sc=sqrtf((float)m.c.d_model); for(auto&v:fbank)v*=sc; add_posenc(fbank,T,F); ggml_backend_t be=ggml_backend_cpu_init(); ggml_init_params cp={(size_t)1024*1024*1024,nullptr,true}; ggml_context*c=ggml_init(cp); ggml_tensor*inp=ggml_new_tensor_2d(c,GGML_TYPE_F32,F,T); ggml_set_input(inp); ggml_tensor*x=sanm_layer(c,m,"audio_encoder.encoders0.0.",inp,T,false); for(int i=0;ine[0]; std::vector out((size_t)Dout*T); ggml_backend_tensor_get(x,out.data(),0,ggml_nbytes(x)); ggml_gallocr_free(ga); ggml_free(c); ggml_backend_free(be); return out; } // ======================= LLM (llama.cpp) ======================= static int decode_batch(llama_context*ctx,int n,llama_token*tok,float*embd,int n_embd,int&n_past,bool last_logits){ std::vector pos(n); std::vector nsid(n,1); std::vector s0(1,0); std::vector sid(n); std::vector lg(n,0); for(int i=0;i wav; if(!funasr_load_audio_16k_mono(wav_path.c_str(),wav)){fprintf(stderr,"failed to read audio\n");return 1;} int64_t t0=ggml_time_us(); enc_model em; if(!load_enc(enc_path.c_str(),em))return 1; ggml_backend_load_all(); llama_model_params mp=llama_model_default_params(); mp.n_gpu_layers=0; llama_model*model=llama_model_load_from_file(llm_path.c_str(),mp); if(!model)return 1; const llama_vocab*vocab=llama_model_get_vocab(model); llama_context_params cp=llama_context_default_params(); cp.n_ctx=2048; cp.n_batch=2048; cp.n_ubatch=2048; llama_context*ctx=llama_init_from_model(model,cp); if(!ctx){fprintf(stderr,"failed to create llama context\n");llama_model_free(model);return 1;} auto sp=llama_sampler_chain_default_params(); llama_sampler*smpl=llama_sampler_chain_init(sp); if(rep!=1.0f) llama_sampler_chain_add(smpl,llama_sampler_init_penalties(256,rep,0.0f,0.0f)); llama_sampler_chain_add(smpl,llama_sampler_init_greedy()); const char*prefix="<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n语音转写:"; const char*suffix="<|im_end|>\n<|im_start|>assistant\n"; auto tokenize=[&](const char*s){int n=-llama_tokenize(vocab,s,strlen(s),nullptr,0,false,true); std::vector v(n); llama_tokenize(vocab,s,strlen(s),v.data(),n,false,true); return v;}; auto pre=tokenize(prefix); auto suf=tokenize(suffix); // Build the list of [offset,len] windows to transcribe (in samples). // --vad : FSMN-VAD speech segments (single-binary front end, replaces fixed chunking) // --chunk sec : fixed-size chunks ; otherwise the whole file in one window std::vector> wins; // {sample offset, sample len} if(!vad_path.empty()){ std::vector> segs; // ms if(!funasr_vad_segments(vad_path,wav,vad_maxseg,segs)){fprintf(stderr,"vad failed\n");return 1;} for(auto&s:segs){ int off=(int)((int64_t)s.first*16000/1000), end=(int)((int64_t)s.second*16000/1000); if(end>(int)wav.size())end=wav.size(); if(end-off>0) wins.push_back({off,end-off}); } fprintf(stderr,"[vad] %zu segments\n",wins.size()); } else { int chunk_n = chunk_sec > 0 ? std::max(1, (int)(chunk_sec*16000)) : (int)wav.size(); for(size_t off=0; off seg(wav.begin()+off, wav.begin()+off+len); int T=0; auto fbank=compute_fbank(seg,T); int D=0; auto adp=run_encoder(em,fbank,T,560,D); int ol=1+(T-3+2)/2; ol=1+(ol-3+2)/2; int n_aud=(ol-1)/2+1; llama_memory_clear(llama_get_memory(ctx), true); // fresh context per chunk int n_past=0; decode_batch(ctx,pre.size(),pre.data(),nullptr,0,n_past,false); decode_batch(ctx,n_aud,nullptr,adp.data(),D,n_past,false); decode_batch(ctx,suf.size(),suf.data(),nullptr,0,n_past,true); llama_token tk=llama_sampler_sample(smpl,ctx,-1); for(int i=0;i0) full.append(buf,k); decode_batch(ctx,1,&tk,nullptr,0,n_past,true); tk=llama_sampler_sample(smpl,ctx,-1); } } printf("%s\n", full.c_str()); int64_t t2=ggml_time_us(); fprintf(stderr,"[done] %.2fs ; chunk=%.0fs\n",(t2-t0)/1e6, chunk_sec); llama_sampler_free(smpl); llama_free(ctx); llama_model_free(model); if(em.ctx_w) ggml_free(em.ctx_w); return 0; }