Qwen3ASR add vad data recognition

This commit is contained in:
jhqxxx
2026-04-17 13:21:51 +08:00
parent baf58fc7ca
commit 1cf075fd1d
14 changed files with 221 additions and 53 deletions
+6 -11
View File
@@ -49,6 +49,12 @@ aha is a high-performance, cross-platform AI inference engine built with Rust an
- **🧠 Attention Optimization** - Optional Flash Attention support for optimized long sequence processing - **🧠 Attention Optimization** - Optional Flash Attention support for optimized long sequence processing
## Changelog ## Changelog
### 2026-04-17
- Qwen3ASR add vad data recognition
### 2026-04-16
- fix FireRedVAD fsmn cache bug
### 2026-04-15 ### 2026-04-15
- add FireRedVAD - add FireRedVAD
@@ -71,17 +77,6 @@ aha is a high-performance, cross-platform AI inference engine built with Rust an
- \<think\>...\</think\> The content of the thought chain is returned using the reasoning_content field. - \<think\>...\</think\> The content of the thought chain is returned using the reasoning_content field.
- chat response add time info - chat response add time info
### 2026-04-01
- refactor deepseek_ocr/fun_asr_nano generate code
### 2026-03-31
- add server and cli mod
- aha model name use modelscope id replace
- update WhichModel
- Usage add time info
- dependencies delete aha_openai_dive,chrono
**[View full changelog](docs/changelog.md)** → **[View full changelog](docs/changelog.md)** →
+9 -15
View File
@@ -47,16 +47,21 @@ aha 是一款基于 Rust 和 Candle 框架构建的高性能跨平台 AI 推理
- **🧠 注意力优化** - 可选 Flash Attention 支持,优化长序列处理 - **🧠 注意力优化** - 可选 Flash Attention 支持,优化长序列处理
## 更新日志 ## 更新日志
### 2026-04-17
- Qwen3ASR 增加 vad 数据识别
### 2026-04-16
- 修复 FireRedVAD fsmn 缓存问题
### 2026-04-15
- 添加 FireRedVAD
### 2026-04-10 ### 2026-04-10
- 修复 LiquidAI/LFM2.5-VL-450M chat_template 加载bug - 修复 LiquidAI/LFM2.5-VL-450M chat_template 加载bug
### 2026-04-08 ### 2026-04-08
- 添加 VoxCPM2 - 添加 VoxCPM2
## Changelog
### 2026-04-15
- 添加 FireRedVAD
### 0.2.5 (2026-04-06) ### 0.2.5 (2026-04-06)
- 添加 qwen3-embedding/qwen3-reranker/all-minilm-l6-v2 - 添加 qwen3-embedding/qwen3-reranker/all-minilm-l6-v2
@@ -70,17 +75,6 @@ aha 是一款基于 Rust 和 Candle 框架构建的高性能跨平台 AI 推理
- \<think\>...\</think\> 思维链内容使用reasoning_content字段返回。 - \<think\>...\</think\> 思维链内容使用reasoning_content字段返回。
- 对话返回添加耗时信息 - 对话返回添加耗时信息
### 2026-04-01
- 重构 deepseek_ocr/fun_asr_nano 生成代码
### 2026-03-31
- 新增 server 和 cli 模块
- aha模型名称使用 modelscope id 替换
- 更新 WhichModel 枚举
- Usage 增加时间信息
- 删除 aha_openai_dive, chrono 依赖
**[查看完整更新日志](docs/changelog.zh-CN.md)** → **[查看完整更新日志](docs/changelog.zh-CN.md)** →
+6
View File
@@ -5,6 +5,12 @@ All notable changes to aha will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### 2026-04-17
- Qwen3ASR add vad data recognition
### 2026-04-16
- fix FireRedVAD fsmn cache bug
### 2026-04-15 ### 2026-04-15
- add FireRedVAD - add FireRedVAD
+6
View File
@@ -5,6 +5,12 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/) 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/spec/v2.0.0.html)。
### 2026-04-17
- Qwen3ASR 增加 vad 数据识别
### 2026-04-16
- 修复 FireRedVAD fsmn 缓存问题
### 2026-04-15 ### 2026-04-15
- 添加 FireRedVAD - 添加 FireRedVAD
+26
View File
@@ -117,6 +117,32 @@ fn sample_and_push(
generated.push(token); generated.push(token);
Ok(token) Ok(token)
} }
pub fn generate_generic_text<M: InferenceModel>(
model: &mut M,
tokenizer: &TokenizerModel,
input_ids: Tensor,
data: MultiModalData,
ctx: &mut GenerationContext,
) -> Result<String> {
let mut generated = Vec::new();
let eos_ids = model.stop_token_ids();
let logits = model.forward_initial(&input_ids, ctx.seqlen_offset, data)?;
let next_token = sample_and_push(ctx, &logits, &mut generated)?;
let mut input_ids = ctx.prepare_for_next_token(next_token)?;
// 自回归循环
for _ in 1..ctx.sample_len {
let logits = model.forward_step(&input_ids, ctx.seqlen_offset)?;
let next_token = sample_and_push(ctx, &logits, &mut generated)?;
if eos_ids.contains(&next_token) {
break;
}
input_ids = ctx.prepare_for_next_token(next_token)?;
}
let text = tokenizer.token_decode(generated)?;
Ok(text)
}
pub fn generate_generic<M: InferenceModel>( pub fn generate_generic<M: InferenceModel>(
model: &mut M, model: &mut M,
+33
View File
@@ -12,6 +12,39 @@ use crate::{
utils::tensor_utils::{pad_replicate_last_dim, prepare_causal_attention_mask, repeat_kv}, utils::tensor_utils::{pad_replicate_last_dim, prepare_causal_attention_mask, repeat_kv},
}; };
#[derive(Debug)]
pub struct VadFrameResult {
pub is_speech: bool,
pub is_speech_start: bool,
pub is_i16: bool,
pub orig_audio: Option<Tensor>,
pub kaldi_audio: Option<Tensor>,
pub model_name: String,
pub mode: String,
}
#[derive(Debug)]
pub struct AsrResult {
pub is_empty: bool,
pub text: Option<String>,
}
impl AsrResult {
pub fn init_empty() -> Self {
AsrResult {
is_empty: true,
text: None,
}
}
pub fn init(text: String) -> Self {
AsrResult {
is_empty: false,
text: Some(text),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GateUpDownMLP { pub struct GateUpDownMLP {
gate_proj: Linear, gate_proj: Linear,
+9 -13
View File
@@ -3,10 +3,13 @@ use candle_core::{D, DType, Device, Tensor};
use candle_nn::VarBuilder; use candle_nn::VarBuilder;
use crate::{ use crate::{
models::fire_red_vad::{ models::{
config::{DetectModelConfig, FireRedVadConfig}, common::modules::VadFrameResult,
model::DetectModel, fire_red_vad::{
processor::{AudioFeat, VadPostprocessor}, config::{DetectModelConfig, FireRedVadConfig},
model::DetectModel,
processor::{AudioFeat, VadPostprocessor},
},
}, },
utils::{ utils::{
audio_utils::{resample_audio_from_bytes, resample_audio_from_vec_f32}, audio_utils::{resample_audio_from_bytes, resample_audio_from_vec_f32},
@@ -23,15 +26,6 @@ pub struct VadResult {
pub mode: String, pub mode: String,
} }
#[derive(Debug)]
pub struct VadFrameResult {
pub is_speech: bool,
pub orig_audio: Option<Tensor>,
pub kaldi_audio: Option<Tensor>,
pub model_name: String,
pub mode: String,
}
pub struct FireRedVad { pub struct FireRedVad {
audio_feat: AudioFeat, audio_feat: AudioFeat,
vad_model: DetectModel, vad_model: DetectModel,
@@ -107,6 +101,8 @@ impl FireRedVad {
if preds_sum as f32 > probs.dim(0)? as f32 * self.cfg.speech_threshold { if preds_sum as f32 > probs.dim(0)? as f32 * self.cfg.speech_threshold {
Ok(Some(VadFrameResult { Ok(Some(VadFrameResult {
is_speech: true, is_speech: true,
is_i16: true,
is_speech_start: false, // TODO: is start speech, asr to clear cache
orig_audio: Some(audio_frame.clone()), orig_audio: Some(audio_frame.clone()),
kaldi_audio: Some(feats), kaldi_audio: Some(feats),
model_name: self.model_name.clone(), model_name: self.model_name.clone(),
+1 -1
View File
@@ -1455,7 +1455,7 @@ impl InferenceModel for Qwen3_5Model {
) -> Result<Tensor> { ) -> Result<Tensor> {
if data.data_vec.len() != 4 { if data.data_vec.len() != 4 {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Lfm2VL process data error, must have pixel_values, image_grid_thw, pixel_values_video, video_grid_thw" "Qwen3.5 process data error, must have pixel_values, image_grid_thw, pixel_values_video, video_grid_thw"
)); ));
} }
let pixel_values = &data.data_vec[0]; let pixel_values = &data.data_vec[0];
+43 -2
View File
@@ -1,7 +1,11 @@
use std::time::Instant; use std::time::Instant;
use crate::{ use crate::{
models::common::generate::get_logit_processor, models::common::{
MultiModalData,
generate::{GenerationContext, generate_generic_text, get_logit_processor},
modules::{AsrResult, VadFrameResult},
},
params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse}, params::chat::{ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse},
utils::response_utils::{build_chunk_response_with_usage, build_completion_response_with_time}, utils::response_utils::{build_chunk_response_with_usage, build_completion_response_with_time},
}; };
@@ -39,6 +43,7 @@ pub struct Qwen3AsrGenerateModel<'a> {
eos_token_id2: u32, eos_token_id2: u32,
generation_config: Qwen3ASRGenerationConfig, generation_config: Qwen3ASRGenerationConfig,
model_name: String, model_name: String,
default_template: String,
} }
impl<'a> Qwen3AsrGenerateModel<'a> { impl<'a> Qwen3AsrGenerateModel<'a> {
@@ -59,7 +64,7 @@ impl<'a> Qwen3AsrGenerateModel<'a> {
let dtype = get_dtype(dtype, cfg_dtype); let dtype = get_dtype(dtype, cfg_dtype);
let model_list = find_type_files(path, "safetensors")?; let model_list = find_type_files(path, "safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? }; let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? };
let qwen3_asr = Qwen3ASRModel::new(vb, &cfg)?; let qwen3_asr = Qwen3ASRModel::new(vb, &cfg, generation_config.eos_token_id.clone())?;
let model_name = std::path::Path::new(path) let model_name = std::path::Path::new(path)
.file_name() .file_name()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
@@ -76,8 +81,44 @@ impl<'a> Qwen3AsrGenerateModel<'a> {
eos_token_id2: generation_config.eos_token_id[1] as u32, eos_token_id2: generation_config.eos_token_id[1] as u32,
generation_config, generation_config,
model_name, model_name,
default_template: "<|im_start|>system\n<|im_end|>\n<|im_start|>user\n<|audio_start|><|audio_pad|><|audio_end|><|im_end|>\n<|im_start|>assistant\n".to_string(),
}) })
} }
pub fn audio_recognize(&mut self, vad_res: VadFrameResult) -> Result<AsrResult> {
if !vad_res.is_speech || vad_res.orig_audio.is_none() {
return Ok(AsrResult::init_empty());
}
if vad_res.is_speech_start {
self.qwen3_asr.clear_kv_cache();
}
let audio_data =
self.processor
.process_vad_res(&self.default_template, vad_res, &self.tokenizer)?;
let input_ids = audio_data.input_ids.clone();
let input_features = Some(audio_data.input_features.clone().to_dtype(self.dtype)?);
let mut ctx = GenerationContext::new(
None,
None,
None,
None,
None,
32432,
input_ids.dim(1)?,
512,
self.device.clone(),
);
let data_vec = vec![input_features];
let data = MultiModalData::new(data_vec);
let text = generate_generic_text(
&mut self.qwen3_asr,
&self.tokenizer,
input_ids,
data,
&mut ctx,
)?;
Ok(AsrResult::init(text))
}
} }
impl<'a> GenerateModel for Qwen3AsrGenerateModel<'a> { impl<'a> GenerateModel for Qwen3AsrGenerateModel<'a> {
+39 -3
View File
@@ -7,7 +7,10 @@ use candle_nn::{
use crate::{ use crate::{
models::{ models::{
common::modules::{NaiveAttention, get_conv2d, get_layer_norm}, common::{
InferenceModel,
modules::{NaiveAttention, get_conv2d, get_layer_norm},
},
qwen3::model::Qwen3DecoderLayer, qwen3::model::Qwen3DecoderLayer,
qwen3_asr::{ qwen3_asr::{
config::{ config::{
@@ -364,12 +367,16 @@ impl Qwen3ASRThinker {
pub struct Qwen3ASRModel { pub struct Qwen3ASRModel {
thinker: Qwen3ASRThinker, thinker: Qwen3ASRThinker,
stop_token_ids: Vec<u32>,
} }
impl Qwen3ASRModel { impl Qwen3ASRModel {
pub fn new(vb: VarBuilder, config: &Qwen3ASRConfig) -> Result<Self> { pub fn new(vb: VarBuilder, config: &Qwen3ASRConfig, eos_ids: Vec<u32>) -> Result<Self> {
let thinker = Qwen3ASRThinker::new(vb.pp("thinker"), &config.thinker_config)?; let thinker = Qwen3ASRThinker::new(vb.pp("thinker"), &config.thinker_config)?;
Ok(Self { thinker }) Ok(Self {
thinker,
stop_token_ids: eos_ids,
})
} }
pub fn forward( pub fn forward(
@@ -387,3 +394,32 @@ impl Qwen3ASRModel {
self.thinker.clear_kv_cache(); self.thinker.clear_kv_cache();
} }
} }
impl InferenceModel for Qwen3ASRModel {
fn forward_initial(
&mut self,
input_ids: &Tensor,
seqlen_offset: usize,
data: crate::models::common::MultiModalData,
) -> Result<Tensor> {
if data.data_vec.len() != 1 {
return Err(anyhow::anyhow!(
"Qwen3 asr process data error, must have pixel_values, image_grid_thw, pixel_values_video, video_grid_thw"
));
}
let input_features = &data.data_vec[0];
self.forward(input_ids, seqlen_offset, input_features.as_ref())
}
fn forward_step(&mut self, input_ids: &Tensor, seqlen_offset: usize) -> Result<Tensor> {
self.forward(input_ids, seqlen_offset, None)
}
fn clear_cache(&mut self) {
self.clear_kv_cache();
}
fn stop_token_ids(&self) -> Vec<u32> {
self.stop_token_ids.clone()
}
}
+39 -4
View File
@@ -1,7 +1,8 @@
use crate::{ use crate::{
models::common::modules::float_range_normalize, params::chat::ChatCompletionParameters, models::common::modules::{VadFrameResult, float_range_normalize},
params::chat::ChatCompletionParameters,
}; };
use anyhow::Result; use anyhow::{Result, anyhow};
use candle_core::{Device, Tensor}; use candle_core::{Device, Tensor};
use crate::{ use crate::{
@@ -81,7 +82,7 @@ impl Qwen3AsrProcessor {
}) })
} }
pub fn process_audio(&self, mes: &ChatCompletionParameters) -> Result<Vec<Tensor>> { pub fn extract_audio_vec(&self, mes: &ChatCompletionParameters) -> Result<Vec<Tensor>> {
let audio_tensors = extract_audios(mes, &self.device, Some(self.sample_rate))?; let audio_tensors = extract_audios(mes, &self.device, Some(self.sample_rate))?;
audio_tensors.iter().map(float_range_normalize).collect() audio_tensors.iter().map(float_range_normalize).collect()
} }
@@ -96,6 +97,40 @@ impl Qwen3AsrProcessor {
text.replace("<|audio_placeholder|>", &self.audio_token) text.replace("<|audio_placeholder|>", &self.audio_token)
} }
pub fn process_vad_res(
&self,
render: &str,
vad_res: VadFrameResult,
tokenizer: &TokenizerModel,
) -> Result<AudioData> {
if let Some(audio) = &vad_res.orig_audio {
let audio_len = audio.dim(0)? as f32;
if audio_len > self.sample_rate as f32 * self.max_asr_input_seconds {
return Err(anyhow!("vad_res orig_audio is too long!"));
}
let mut audio = audio.unsqueeze(0)?;
if vad_res.is_i16 {
audio = audio.affine(1.0 / 32768.0, 0.0)?;
}
audio = float_range_normalize(&audio)?;
let (input_features, _) =
self.whisper_feature_extracor
.call(&audio, self.sample_rate, false)?;
let audio_len = input_features.dim(2)?;
let output_len = get_feat_extract_output_lengths(audio_len);
let text = self.replace_special_tokens(render, output_len);
let input_ids = tokenizer.text_encode(text, &self.device)?;
let input_features = input_features.squeeze(0)?;
let audio_data = AudioData {
input_features,
input_ids,
};
Ok(audio_data)
} else {
Err(anyhow!("vad_res orig_audio is none!"))
}
}
pub fn process_info( pub fn process_info(
&self, &self,
mes: &ChatCompletionParameters, mes: &ChatCompletionParameters,
@@ -122,7 +157,7 @@ impl Qwen3AsrProcessor {
render = format!("{}language {}'<asr_text>'", render, lang); render = format!("{}language {}'<asr_text>'", render, lang);
} }
} }
let audio_tensors = self.process_audio(mes)?; let audio_tensors = self.extract_audio_vec(mes)?;
let audio_len = audio_tensors.len(); let audio_len = audio_tensors.len();
if audio_len != audio_count { if audio_len != audio_count {
return Err(anyhow::anyhow!("audio_pad num != audio num")); return Err(anyhow::anyhow!("audio_pad num != audio num"));
+1 -1
View File
@@ -21,7 +21,7 @@ fn fun_asr_nano_generate() -> Result<()> {
"type": "audio", "type": "audio",
"audio_url": "audio_url":
{ {
"url": "file://./assets/audio/zh.mp3" "url": "file://./assets/audio/voice_01.wav"
} }
}, },
{ {
+1 -1
View File
@@ -22,7 +22,7 @@ fn glm_asr_nano_generate() -> Result<()> {
"type": "audio", "type": "audio",
"audio_url": "audio_url":
{ {
"url": "file://./assets/audio/zh.mp3" "url": "file://./assets/audio/voice_01.wav"
} }
}, },
{ {
+2 -2
View File
@@ -6,7 +6,7 @@ use anyhow::Result;
use rocket::futures::StreamExt; use rocket::futures::StreamExt;
#[test] #[test]
fn qwen3_asr_generate() -> Result<()> { fn qwen3_asr_generate() -> Result<()> {
// RUST_BACKTRACE=1 cargo test -F cuda qwen3_asr_generate -r -- --nocapture // RUST_BACKTRACE=1 cargo test -F cuda --test test_qwen3_asr qwen3_asr_generate -r -- --nocapture
let save_dir = let save_dir =
aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?; aha::utils::get_default_save_dir().ok_or(anyhow::anyhow!("Failed to get save dir"))?;
let model_path = format!("{}/Qwen/Qwen3-ASR-0.6B/", save_dir); //Qwen/Qwen3-ASR-1.7B let model_path = format!("{}/Qwen/Qwen3-ASR-0.6B/", save_dir); //Qwen/Qwen3-ASR-1.7B
@@ -21,7 +21,7 @@ fn qwen3_asr_generate() -> Result<()> {
"type": "audio", "type": "audio",
"audio_url": "audio_url":
{ {
"url": "https://package-release.coderbox.cn/aiway/test/other/%E5%93%AA%E5%90%92.wav" "url": "file://./assets/audio/voice_01.wav"
} }
} }
] ]