From 6ef8facfde71ee714dcae8a145d7f3cda60d3f8a Mon Sep 17 00:00:00 2001 From: jhqxxx <18280426169@163.com> Date: Wed, 7 Jan 2026 21:46:01 +0800 Subject: [PATCH] add glm-asr-nano --- Cargo.lock | 220 +++++++++- Cargo.toml | 9 +- README.md | 8 +- assets/audio/zh.mp3 | Bin 0 -> 44973 bytes src/main.rs | 1 + src/models/common/mod.rs | 211 +++++++++- src/models/deepseek_ocr/model.rs | 2 +- src/models/glm_asr_nano/config.rs | 68 +++- src/models/glm_asr_nano/generate.rs | 167 +++++++- src/models/glm_asr_nano/mod.rs | 2 +- src/models/glm_asr_nano/model.rs | 320 +++++++++++++++ src/models/glm_asr_nano/processor.rs | 233 ++++++++--- src/models/mod.rs | 12 +- src/models/qwen3vl/model.rs | 1 + src/models/rmbg2_0/model.rs | 2 +- src/models/voxcpm/model.rs | 7 +- src/position_embed/rope.rs | 43 ++ src/utils/audio_utils.rs | 582 ++++++++++++++++++++++++++- src/utils/tensor_utils.rs | 26 ++ tests/messy_test.rs | 28 +- tests/test_glm_asr_nano.rs | 74 +++- tests/weight_test.rs | 19 + 22 files changed, 1918 insertions(+), 117 deletions(-) create mode 100644 assets/audio/zh.mp3 diff --git a/Cargo.lock b/Cargo.lock index fdb881a..42320ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aha" -version = "0.1.6" +version = "0.1.7" dependencies = [ "aha_openai_dive", "anyhow", @@ -38,10 +38,12 @@ dependencies = [ "modelscope", "num", "rayon", + "realfft", "reqwest", "rocket", "serde", "serde_json", + "symphonia", "tokenizers", "tokio", "url", @@ -896,7 +898,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1002,7 +1004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1040,6 +1042,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "fancy-regex" version = "0.13.0" @@ -1802,7 +1810,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "system-configuration", "tokio", "tower-service", @@ -2066,7 +2074,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2457,7 +2465,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2812,6 +2820,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -3095,6 +3112,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + [[package]] name = "reborrow" version = "0.5.5" @@ -3345,6 +3371,20 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustfft" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", +] + [[package]] name = "rustix" version = "1.1.2" @@ -3355,7 +3395,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3677,6 +3717,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strsim" version = "0.11.1" @@ -3689,6 +3735,152 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "2.0.108" @@ -3798,7 +3990,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4187,6 +4379,16 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -4524,7 +4726,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fb504d7..65f9fcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "aha" -version = "0.1.6" +version = "0.1.7" edition = "2024" repository = "https://github.com/jhqxxx/aha" license = "Apache-2.0" -description = "aha model inference library, now supports Qwen2.5VL, MiniCPM4, VoxCPM, Qwen3VL, DeepSeek-OCR, Hunyuan-OCR, PaddleOCR-VL, VoxCPM1.5, RMBG2.0" +description = "aha model inference library, now supports Qwen2.5VL, MiniCPM4, VoxCPM, Qwen3VL, DeepSeek-OCR, Hunyuan-OCR, PaddleOCR-VL, VoxCPM1.5, RMBG2.0, GLM-ASR-Nano-2512" [dependencies] candle-core = { version = "0.9.1"} @@ -32,6 +32,10 @@ modelscope = "0.1.0" dirs = "6.0.0" url = "2.5.7" rayon = "1.10" +# rubato = "1.0.0" +# audioadapter-buffers = "2.0.0" +realfft = "3.5.0" +symphonia = { version = "0.5.5", features = ["mp3", "wav"] } [features] flash-attn=["candle-flash-attn"] @@ -41,3 +45,4 @@ ffmpeg=["ffmpeg-next"] [lints.clippy] needless_range_loop = "allow" single_range_in_vec_init = "allow" +manual_div_ceil = "allow" diff --git a/README.md b/README.md index 8218c0e..68dfd13 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ - 模型:[VoxCPM1.5](https://huggingface.co/openbmb/VoxCPM1.5) 开源协议:[Apache license 2.0](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/apache-2.0.md) * [RMBG2.0](https://huggingface.co/collections/briaai/rmbg) - RMBGv2.0由BRIA AI开发,供非商业用途使用。 - 模型:[RMBG2.0](https://huggingface.co/briaai/RMBG-2.0) 开源协议:[Attribution-NonCommercial 4.0 International](https://creativecommons.org/licenses/by-nc/4.0/deed.en) +* GLM-ASR-Nano-2512 - 智谱AI语音识别模型 + - 模型:[GLM-ASR-Nano-2512](https://huggingface.co/zai-org/GLM-ASR-Nano-2512) 开源协议:[MIT](https://huggingface.co/datasets/choosealicense/licenses/blob/main/markdown/mit.md) ## 计划支持 我们持续扩展支持的模型列表,欢迎贡献! @@ -114,6 +116,7 @@ cargo run -F cuda -r -- [参数] * RMBG2.0: AI-ModelScope/RMBG-2.0 模型 * voxcpm: OpenBMB/VoxCPM-0.5B 模型 * voxcpm1.5: OpenBMB/VoxCPM1.5 模型 + * glm-asr-nano-2512: ZhipuAI/GLM-ASR-Nano-2512 模型 * 示例:--model deepseek-ocr 或 -m qwen3vl-2b 3. 权重路径 @@ -150,7 +153,7 @@ cargo run -F cuda -r -- [参数] 1. 对话接口 - **端点**: `POST /chat/completions` - **功能**: 多模态对话和文本生成 -- **支持模型**: Qwen2.5VL,Qwen3VL,DeepSeekOCR 等 +- **支持模型**: Qwen2.5VL,Qwen3VL,DeepSeekOCR, GLM-ASR-Nano-2512 等 - **请求格式**: OpenAI Chat Completion 格式 - **响应格式**: OpenAI Chat Completion 格式 - **流式支持**: 支持 @@ -286,6 +289,9 @@ cargo test -F cuda voxcpm_generate -r -- --nocapture 2. 提交新的 Issue,包含详细描述和复现步骤 ## 更新日志 +### v0.1.7 +* 支持GLM-ASR-Nano-2512 模型 + ### v0.1.6 * 支持RMGB2.0 模型 diff --git a/assets/audio/zh.mp3 b/assets/audio/zh.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..1ae2c89b29112ee5e23bcebc353ea6687c38e6bd GIT binary patch literal 44973 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 h|EH_o@=u_Nv%?9@#L?KINYO|%0Qo3{R1{|8>Fc anyhow::Result<()> { WhichModel::RMBG2_0 => "AI-ModelScope/RMBG-2.0", WhichModel::VoxCPM => "OpenBMB/VoxCPM-0.5B", WhichModel::VoxCPM1_5 => "OpenBMB/VoxCPM1.5", + WhichModel::GlmASRNano2512 => "ZhipuAI/GLM-ASR-Nano-2512", }; let model_path = match &args.weight_path { Some(path) => path.clone(), diff --git a/src/models/common/mod.rs b/src/models/common/mod.rs index 9782441..f72672e 100644 --- a/src/models/common/mod.rs +++ b/src/models/common/mod.rs @@ -1,12 +1,16 @@ use anyhow::Result; use candle_core::{D, Tensor}; use candle_nn::{ - Activation, BatchNorm, BatchNormConfig, Conv2d, Conv2dConfig, LayerNorm, LayerNormConfig, - Linear, Module, RmsNorm, VarBuilder, batch_norm, conv2d, conv2d_no_bias, layer_norm, linear, - linear_no_bias, rms_norm, + Activation, BatchNorm, BatchNormConfig, Conv1d, Conv1dConfig, Conv2d, Conv2dConfig, Embedding, + LayerNorm, LayerNormConfig, Linear, Module, RmsNorm, VarBuilder, batch_norm, conv1d, + conv1d_no_bias, conv2d, conv2d_no_bias, embedding, layer_norm, linear, linear_no_bias, + rms_norm, }; -use crate::{position_embed::rope::apply_rotary_pos_emb, utils::tensor_utils::repeat_kv}; +use crate::{ + position_embed::rope::{RoPE, apply_rotary_pos_emb}, + utils::tensor_utils::{prepare_causal_attention_mask, repeat_kv}, +}; #[derive(Debug, Clone)] pub struct GateUpDownMLP { @@ -63,8 +67,11 @@ pub struct TwoLinearMLP { impl TwoLinearMLP { pub fn new( vb: VarBuilder, - embedding_dim: usize, - mlp_dim: usize, + // embedding_dim: usize, + // mlp_dim: usize, + in_dim: usize, + middle_dim: usize, + out_dim: usize, act: Activation, bias: bool, linear1_pp_name: &str, @@ -72,13 +79,13 @@ impl TwoLinearMLP { ) -> Result { let (linear1, linear2) = if bias { ( - linear(embedding_dim, mlp_dim, vb.pp(linear1_pp_name))?, - linear(mlp_dim, embedding_dim, vb.pp(linear2_pp_name))?, + linear(in_dim, middle_dim, vb.pp(linear1_pp_name))?, + linear(middle_dim, out_dim, vb.pp(linear2_pp_name))?, ) } else { ( - linear_no_bias(embedding_dim, mlp_dim, vb.pp(linear1_pp_name))?, - linear_no_bias(mlp_dim, embedding_dim, vb.pp(linear2_pp_name))?, + linear_no_bias(in_dim, middle_dim, vb.pp(linear1_pp_name))?, + linear_no_bias(middle_dim, out_dim, vb.pp(linear2_pp_name))?, ) }; Ok(Self { @@ -304,6 +311,7 @@ impl NaiveAttnTwoLinearMLPBlock { vb.pp(mlp_pp_name), hidden_size, intermediate_size, + hidden_size, hidden_act, mlp_bias, linear1_pp_name, @@ -503,6 +511,32 @@ pub fn get_conv2d( Ok(conv2d) } +pub fn get_conv1d( + vb: VarBuilder, + in_c: usize, + out_c: usize, + kernel_size: usize, + padding: usize, + stride: usize, + dilation: usize, + groups: usize, + bias: bool, +) -> Result { + let cfg = Conv1dConfig { + padding, + stride, + dilation, + groups, + cudnn_fwd_algo: None, + }; + let conv1d = if bias { + conv1d(in_c, out_c, kernel_size, cfg, vb)? + } else { + conv1d_no_bias(in_c, out_c, kernel_size, cfg, vb)? + }; + Ok(conv1d) +} + pub fn get_layer_norm(vb: VarBuilder, eps: f64, dim: usize) -> Result { let ln_config = LayerNormConfig { eps, @@ -620,3 +654,160 @@ pub fn deform_conv2d_kernel( } Ok(out) } + +pub struct LlamaModel { + pub embed_tokens: Embedding, + layers: Vec, + norm: RmsNorm, + rotary_emb: RoPE, +} + +impl LlamaModel { + pub fn new( + vb: VarBuilder, + vocab_size: usize, + hidden_size: usize, + num_hidden_layers: usize, + num_attention_heads: usize, + num_key_value_heads: Option, + head_dim: Option, + attn_bias: bool, + attn_pp_name: &str, + o_proj_pp_name: Option<&str>, + intermediate_size: usize, + hidden_act: Activation, + mlp_bias: bool, + mlp_pp_name: &str, + norm_eps: f64, + input_norm_pp_name: &str, + post_norm_pp_name: &str, + rope_theta_base: f32, + ) -> Result { + let embed_tokens = embedding(vocab_size, hidden_size, vb.pp("embed_tokens"))?; + let mut layers = vec![]; + let vb_layers = vb.pp("layers"); + for i in 0..num_hidden_layers { + let layers_i = NaiveAttnGateUpDownMLPBlock::new( + vb_layers.pp(i), + hidden_size, + num_attention_heads, + num_key_value_heads, + head_dim, + attn_bias, + attn_pp_name, + o_proj_pp_name, + intermediate_size, + hidden_act, + mlp_bias, + mlp_pp_name, + norm_eps, + input_norm_pp_name, + post_norm_pp_name, + )?; + layers.push(layers_i); + } + let norm = rms_norm(hidden_size, norm_eps, vb.pp("norm"))?; + let head_dim = head_dim.unwrap_or(hidden_size / num_attention_heads); + let rotary_emb = RoPE::new(head_dim, rope_theta_base, vb.device())?; + Ok(Self { + embed_tokens, + layers, + norm, + rotary_emb, + }) + } + + pub fn forward(&mut self, inputs_embeds: &Tensor, seqlen_offset: usize) -> Result { + let (b_size, seq_len, _) = inputs_embeds.dims3()?; + + let (cos, sin) = self + .rotary_emb + .forward(seqlen_offset, seq_len, inputs_embeds.device())?; + let mut xs = inputs_embeds.clone(); + let attention_mask: Option = { + if seq_len <= 1 { + None + } else { + Some(prepare_causal_attention_mask( + b_size, + seq_len, + 0, + xs.device(), + )?) + } + }; + for layer in self.layers.iter_mut() { + xs = layer.forward(&xs, &cos, &sin, attention_mask.as_ref())?; + } + let xs = xs.apply(&self.norm)?; + Ok(xs) + } + + pub fn clear_kv_cache(&mut self) { + for layer in self.layers.iter_mut() { + layer.clear_kv_cache() + } + } +} + +pub struct LlamaForCausalLM { + pub model: LlamaModel, + lm_head: Linear, +} + +impl LlamaForCausalLM { + pub fn new( + vb: VarBuilder, + vocab_size: usize, + hidden_size: usize, + num_hidden_layers: usize, + num_attention_heads: usize, + num_key_value_heads: Option, + head_dim: Option, + attn_bias: bool, + attn_pp_name: &str, + o_proj_pp_name: Option<&str>, + intermediate_size: usize, + hidden_act: Activation, + mlp_bias: bool, + mlp_pp_name: &str, + norm_eps: f64, + input_norm_pp_name: &str, + post_norm_pp_name: &str, + rope_theta_base: f32, + ) -> Result { + let model = LlamaModel::new( + vb.pp("model"), + vocab_size, + hidden_size, + num_hidden_layers, + num_attention_heads, + num_key_value_heads, + head_dim, + attn_bias, + attn_pp_name, + o_proj_pp_name, + intermediate_size, + hidden_act, + mlp_bias, + mlp_pp_name, + norm_eps, + input_norm_pp_name, + post_norm_pp_name, + rope_theta_base, + )?; + let lm_head = linear_no_bias(hidden_size, vocab_size, vb.pp("lm_head"))?; + Ok(Self { model, lm_head }) + } + + pub fn forward(&mut self, inputs_embeds: &Tensor, seqlen_offset: usize) -> Result { + let outputs = self.model.forward(inputs_embeds, seqlen_offset)?; + let seq_len = outputs.dim(1)?; + let hidden_state = outputs.narrow(1, seq_len - 1, 1)?; + let logits = self.lm_head.forward(&hidden_state)?; + Ok(logits) + } + pub fn clear_kv_cache(&mut self) { + self.model.clear_kv_cache(); + } +} diff --git a/src/models/deepseek_ocr/model.rs b/src/models/deepseek_ocr/model.rs index 6919afb..26764d1 100644 --- a/src/models/deepseek_ocr/model.rs +++ b/src/models/deepseek_ocr/model.rs @@ -271,7 +271,7 @@ impl Block { )?; let norm2 = get_layer_norm(vb.pp("norm2"), eps, dim)?; let mlp_dim = (dim as f32 * mlp_ratio) as usize; - let mlp = TwoLinearMLP::new(vb.pp("mlp"), dim, mlp_dim, act, true, "lin1", "lin2")?; + let mlp = TwoLinearMLP::new(vb.pp("mlp"), dim, mlp_dim, dim, act, true, "lin1", "lin2")?; Ok(Self { norm1, attn, diff --git a/src/models/glm_asr_nano/config.rs b/src/models/glm_asr_nano/config.rs index 6dacf41..dc29a7a 100644 --- a/src/models/glm_asr_nano/config.rs +++ b/src/models/glm_asr_nano/config.rs @@ -1,4 +1,5 @@ -use serde::{Deserialize}; +use candle_nn::Activation; +use serde::Deserialize; #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct GlmAsrNanoProcessorConfig { @@ -21,4 +22,67 @@ pub struct FeatureExtractor { pub padding_value: f32, pub return_attention_mask: bool, pub sampling_rate: usize, -} \ No newline at end of file +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct GlmAsrNanoConfig { + pub audio_config: GlmAsrAudioConfig, + pub audio_token_id: u32, + pub dtype: String, + pub hidden_size: usize, + pub projector_hidden_act: Activation, + pub text_config: GlmAsrTextConfig, + pub vocab_size: usize, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct GlmAsrAudioConfig { + pub attention_dropout: f64, + pub head_dim: usize, + pub hidden_act: Activation, + pub hidden_size: usize, + pub initializer_range: f64, + pub intermediate_size: usize, + pub max_position_embeddings: usize, + pub num_attention_heads: usize, + pub num_hidden_layers: usize, + pub num_key_value_heads: usize, + pub num_mel_bins: usize, + pub partial_rotary_factor: f64, + pub rope_parameters: GlmAsrRopeParameters, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct GlmAsrRopeParameters { + pub partial_rotary_factor: f64, + pub rope_theta: f32, + pub rope_type: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct GlmAsrTextConfig { + pub attention_bias: bool, + pub attention_dropout: f64, + pub eos_token_id: Vec, + pub head_dim: usize, + pub hidden_act: Activation, + pub hidden_size: usize, + pub initializer_range: f64, + pub intermediate_size: usize, + pub max_position_embeddings: usize, + pub mlp_bias: bool, + pub num_attention_heads: usize, + pub num_hidden_layers: usize, + pub num_key_value_heads: usize, + pub pretraining_tp: usize, + pub rms_norm_eps: f64, + pub rope_parameters: GlmAsrTextRopeParameters, + pub use_cache: bool, + pub vocab_size: usize, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct GlmAsrTextRopeParameters { + pub rope_theta: f32, + pub rope_type: String, +} diff --git a/src/models/glm_asr_nano/generate.rs b/src/models/glm_asr_nano/generate.rs index dc73896..5674531 100644 --- a/src/models/glm_asr_nano/generate.rs +++ b/src/models/glm_asr_nano/generate.rs @@ -1,24 +1,37 @@ -use aha_openai_dive::v1::resources::chat::ChatCompletionParameters; -use anyhow::Result; -use candle_core::{DType, Device}; +use aha_openai_dive::v1::resources::chat::{ + ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse, +}; +use anyhow::{Result, anyhow}; +use candle_core::{DType, Device, Tensor}; +use candle_nn::VarBuilder; +use rocket::async_stream::stream; +use rocket::futures::Stream; use crate::{ chat_template::ChatTemplate, - models::glm_asr_nano::{config::GlmAsrNanoProcessorConfig, processor::GlmAsrNanoProcessor}, + models::{ + GenerateModel, + glm_asr_nano::{ + config::GlmAsrNanoConfig, model::GlmAsrNanoModel, processor::GlmAsrNanoProcessor, + }, + }, tokenizer::TokenizerModel, - utils::{get_device, get_dtype}, + utils::{ + build_completion_chunk_response, build_completion_response, find_type_files, get_device, + get_dtype, get_logit_processor, + }, }; pub struct GlmAsrNanoGenerateModel<'a> { chat_template: ChatTemplate<'a>, tokenizer: TokenizerModel, processor: GlmAsrNanoProcessor, - // glm_asr_nano: GlmAsrNanoModel, + glm_asr_nano: GlmAsrNanoModel, device: Device, - // eos_token_id1: u32, - // eos_token_id2: u32, - // eos_token_id3: u32, - // generation_config: GlmAsrNanoGenerationConfig, + dtype: DType, + eos_token_id1: u32, + eos_token_id2: u32, + eos_token_id3: u32, model_name: String, } @@ -28,23 +41,141 @@ impl<'a> GlmAsrNanoGenerateModel<'a> { let tokenizer = TokenizerModel::init(path)?; let device = get_device(device); let processor = GlmAsrNanoProcessor::new(path, &device, DType::F32)?; - // let cfg_dtype = cfg.dtype.as_str(); - // let dtype = get_dtype(dtype, cfg_dtype); + let config_path = path.to_string() + "/config.json"; + let cfg: GlmAsrNanoConfig = serde_json::from_slice(&std::fs::read(config_path)?)?; + let cfg_dtype = cfg.dtype.as_str(); + let dtype = get_dtype(dtype, cfg_dtype); + let model_list = find_type_files(path, "safetensors")?; + let vb = unsafe { VarBuilder::from_mmaped_safetensors(&model_list, dtype, &device)? }; + let glm_asr_nano = GlmAsrNanoModel::new(vb, cfg)?; Ok(Self { chat_template, tokenizer, processor, + glm_asr_nano, device, - // eos_token_id1, - // eos_token_id2, - // eos_token_id3, + dtype, + eos_token_id1: 59246, + eos_token_id2: 59253, + eos_token_id3: 59255, model_name: "glm-asr-nano".to_string(), }) } +} - pub fn generate(&self, mes: ChatCompletionParameters) -> Result<()> { +impl<'a> GenerateModel for GlmAsrNanoGenerateModel<'a> { + fn generate(&mut self, mes: ChatCompletionParameters) -> Result { + let seed = match mes.seed { + None => 34562u64, + Some(s) => s as u64, + }; + let mut logit_processor = get_logit_processor(mes.temperature, mes.top_p, None, seed); let render_text = self.chat_template.apply_chat_template(&mes)?; - let audio = self.processor.process_info(&mes)?; - Ok(()) + let (input_features, audio_token_lengths, replace_text) = + self.processor.process_info(&mes, &render_text)?; + let mut input_ids = self.tokenizer.text_encode(replace_text, &self.device)?; + let mut input_features = Some(input_features.to_dtype(self.dtype)?); + let mut audio_token_lengths = Some(audio_token_lengths); + let mut seq_len = input_ids.dim(1)?; + let mut seqlen_offset = 0; + let mut generate: Vec = Vec::new(); + let sample_len = mes.max_tokens.unwrap_or(1024); + for _ in 0..sample_len { + let logits = self.glm_asr_nano.forward( + input_features.as_ref(), + audio_token_lengths.as_ref(), + &input_ids, + seqlen_offset, + )?; + let logits = logits.squeeze(0)?.squeeze(0)?.to_dtype(DType::F32)?; + let next_token = logit_processor.sample(&logits)?; + generate.push(next_token); + if next_token == self.eos_token_id1 + || next_token == self.eos_token_id2 + || next_token == self.eos_token_id3 + { + break; + } + seqlen_offset += seq_len; + seq_len = 1; + input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device)?; + input_features = None; + audio_token_lengths = None; + } + let num_token = generate.len() as u32; + let res = self.tokenizer.token_decode(generate)?; + self.glm_asr_nano.clear_kv_cache(); + let response = build_completion_response(res, &self.model_name, Some(num_token)); + Ok(response) + } + + fn generate_stream( + &mut self, + mes: ChatCompletionParameters, + ) -> Result< + Box< + dyn Stream> + + Send + + Unpin + + '_, + >, + > { + let seed = match mes.seed { + None => 34562u64, + Some(s) => s as u64, + }; + let mut logit_processor = get_logit_processor(mes.temperature, mes.top_p, None, seed); + let render_text = self.chat_template.apply_chat_template(&mes)?; + let (input_features, audio_token_lengths, replace_text) = + self.processor.process_info(&mes, &render_text)?; + let input_ids = self.tokenizer.text_encode(replace_text, &self.device)?; + + let mut seq_len = input_ids.dim(1)?; + let mut seqlen_offset = 0; + let sample_len = mes.max_tokens.unwrap_or(1024); + let stream = stream! { + let mut error_tokens = Vec::new(); + let mut input_features = Some(input_features.to_dtype(self.dtype)?); + let mut audio_token_lengths = Some(audio_token_lengths); + let mut input_ids = input_ids; + for _ in 0..sample_len { + let logits = + self.glm_asr_nano + .forward(input_features.as_ref(), audio_token_lengths.as_ref(), &input_ids, seqlen_offset)?; + let logits = logits.squeeze(0)?.squeeze(0)?.to_dtype(DType::F32)?; + let next_token = logit_processor.sample(&logits)?; + let mut decode_ids = Vec::new(); + if !error_tokens.is_empty() { + decode_ids.extend_from_slice(&error_tokens); + } + decode_ids.push(next_token); + let decoded_token = self.tokenizer.token_decode(decode_ids).map_err(|e| anyhow!(format!("stream decode error{e}")))?; + if decoded_token.contains("�") { + error_tokens.push(next_token); + if error_tokens.len() > 3 { + error_tokens.clear(); + } + seqlen_offset += seq_len; + seq_len = 1; + input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device)?; + input_features = None; + audio_token_lengths = None; + continue; + } + error_tokens.clear(); + let chunk = build_completion_chunk_response(decoded_token, &self.model_name, None, None); + yield Ok(chunk); + if next_token == self.eos_token_id1 || next_token == self.eos_token_id2 || next_token == self.eos_token_id3{ + break; + } + seqlen_offset += seq_len; + seq_len = 1; + input_ids = Tensor::from_vec(vec![next_token], (1, 1), &self.device)?; + input_features = None; + audio_token_lengths = None; + } + self.glm_asr_nano.clear_kv_cache(); + }; + Ok(Box::new(Box::pin(stream))) } } diff --git a/src/models/glm_asr_nano/mod.rs b/src/models/glm_asr_nano/mod.rs index ce9b2a0..8b1baf7 100644 --- a/src/models/glm_asr_nano/mod.rs +++ b/src/models/glm_asr_nano/mod.rs @@ -1,4 +1,4 @@ pub mod config; pub mod generate; pub mod model; -pub mod processor; \ No newline at end of file +pub mod processor; diff --git a/src/models/glm_asr_nano/model.rs b/src/models/glm_asr_nano/model.rs index e69de29..21e5023 100644 --- a/src/models/glm_asr_nano/model.rs +++ b/src/models/glm_asr_nano/model.rs @@ -0,0 +1,320 @@ +use anyhow::Result; +use candle_core::{IndexOp, Tensor}; +use candle_nn::{Conv1d, LayerNorm, Linear, Module, VarBuilder, linear, linear_no_bias}; + +use crate::{ + models::{ + common::{ + LlamaForCausalLM, TwoLinearMLP, eager_attention_forward, get_conv1d, get_layer_norm, + }, + glm_asr_nano::config::{GlmAsrAudioConfig, GlmAsrNanoConfig}, + }, + position_embed::rope::{RoPE, glm_asr_apply_rotary_pos_emb}, + utils::tensor_utils::{get_equal_mask, masked_scatter_dim0}, +}; + +#[derive(Debug, Clone)] +// pub struct AttentionNobias { +pub struct GlmAsrAttention { + q_proj: Linear, + k_proj: Linear, + v_proj: Linear, + o_proj: Linear, + num_heads: usize, + num_kv_heads: usize, + num_kv_groups: usize, + head_dim: usize, + middle_size: usize, +} + +impl GlmAsrAttention { + pub fn new( + vb: VarBuilder, + hidden_size: usize, + num_attention_heads: usize, + num_key_value_heads: usize, + head_dim: Option, + ) -> Result { + let num_kv_groups = num_attention_heads / num_key_value_heads; + let head_dim = match head_dim { + None => hidden_size / num_attention_heads, + Some(dim) => dim, + }; + let q_proj = linear(hidden_size, num_attention_heads * head_dim, vb.pp("q_proj"))?; + let k_proj = linear_no_bias(hidden_size, num_key_value_heads * head_dim, vb.pp("k_proj"))?; + let v_proj = linear(hidden_size, num_key_value_heads * head_dim, vb.pp("v_proj"))?; + let o_proj = linear(num_attention_heads * head_dim, hidden_size, vb.pp("o_proj"))?; + + Ok(Self { + q_proj, + k_proj, + v_proj, + o_proj, + num_heads: num_attention_heads, + num_kv_heads: num_key_value_heads, + num_kv_groups, + head_dim, + middle_size: num_attention_heads * head_dim, + }) + } + + pub fn forward( + &self, + xs: &Tensor, + cos: Option<&Tensor>, + sin: Option<&Tensor>, + attention_mask: Option<&Tensor>, + tof32: bool, + ) -> Result { + let (b_sz, q_len, _) = xs.dims3()?; + let query_states = self.q_proj.forward(xs)?; + let key_states = self.k_proj.forward(xs)?; + let value_states = self.v_proj.forward(xs)?; + let query_states = query_states + .reshape((b_sz, q_len, self.num_heads, self.head_dim))? + .transpose(1, 2)?; + let key_states = key_states + .reshape((b_sz, q_len, self.num_kv_heads, self.head_dim))? + .transpose(1, 2)?; + let value_states = value_states + .reshape((b_sz, q_len, self.num_kv_heads, self.head_dim))? + .transpose(1, 2)?; + let (query_states, key_states) = if let Some(cos) = cos + && let Some(sin) = sin + { + glm_asr_apply_rotary_pos_emb(&query_states, &key_states, cos, sin, tof32)? + } else { + (query_states, key_states) + }; + + let scale = 1f64 / f64::sqrt(self.head_dim as f64); + let attn_output = eager_attention_forward( + &query_states, + &key_states, + &value_states, + Some(self.num_kv_groups), + attention_mask, + scale, + )?; + let attn_output = attn_output.reshape((b_sz, q_len, self.middle_size))?; + let attn_output = attn_output.apply(&self.o_proj)?; + Ok(attn_output) + } +} + +pub struct GlmAsrEncoderLayer { + self_attn: GlmAsrAttention, + mlp: TwoLinearMLP, + input_layernorm: LayerNorm, + post_attention_layernorm: LayerNorm, +} + +impl GlmAsrEncoderLayer { + pub fn new(vb: VarBuilder, audio_cfg: &GlmAsrAudioConfig) -> Result { + let self_attn = GlmAsrAttention::new( + vb.pp("self_attn"), + audio_cfg.hidden_size, + audio_cfg.num_attention_heads, + audio_cfg.num_key_value_heads, + Some(audio_cfg.head_dim), + )?; + let mlp = TwoLinearMLP::new( + vb.pp("mlp"), + audio_cfg.hidden_size, + audio_cfg.intermediate_size, + audio_cfg.hidden_size, + audio_cfg.hidden_act, + true, + "fc1", + "fc2", + )?; + let input_layernorm = + get_layer_norm(vb.pp("input_layernorm"), 1e-5, audio_cfg.hidden_size)?; + let post_attention_layernorm = get_layer_norm( + vb.pp("post_attention_layernorm"), + 1e-5, + audio_cfg.hidden_size, + )?; + Ok(Self { + self_attn, + mlp, + input_layernorm, + post_attention_layernorm, + }) + } + + pub fn forward( + &self, + xs: &Tensor, + cos: Option<&Tensor>, + sin: Option<&Tensor>, + attention_mask: Option<&Tensor>, + tof32: bool, + ) -> Result { + let residual = xs.clone(); + let xs = self.input_layernorm.forward(xs)?; + let xs = self + .self_attn + .forward(&xs, cos, sin, attention_mask, tof32)?; + let residual = residual.add(&xs)?; + let xs = self.post_attention_layernorm.forward(&residual)?; + let xs = self.mlp.forward(&xs)?; + let xs = residual.add(&xs)?; + Ok(xs) + } +} + +pub struct GlmAsrEncoder { + conv1: Conv1d, + conv2: Conv1d, + layers: Vec, + norm: LayerNorm, + rotary_emb: RoPE, +} + +impl GlmAsrEncoder { + pub fn new(vb: VarBuilder, audio_cfg: &GlmAsrAudioConfig) -> Result { + let conv1 = get_conv1d( + vb.pp("conv1"), + audio_cfg.num_mel_bins, + audio_cfg.hidden_size, + 3, + 1, + 1, + 1, + 1, + true, + )?; + let conv2 = get_conv1d( + vb.pp("conv2"), + audio_cfg.hidden_size, + audio_cfg.hidden_size, + 3, + 1, + 2, + 1, + 1, + true, + )?; + let mut layers = vec![]; + let vb_layers = vb.pp("layers"); + for i in 0..audio_cfg.num_hidden_layers { + let layer_i = GlmAsrEncoderLayer::new(vb_layers.pp(i), audio_cfg)?; + layers.push(layer_i); + } + let norm = get_layer_norm(vb.pp("norm"), 1e-5, audio_cfg.hidden_size)?; + let dim = (audio_cfg.head_dim as f64 * audio_cfg.partial_rotary_factor) as usize; + let rotary_emb = RoPE::new(dim, audio_cfg.rope_parameters.rope_theta, vb.device())?; + Ok(Self { + conv1, + conv2, + layers, + norm, + rotary_emb, + }) + } + pub fn forward(&self, xs: &Tensor) -> Result { + let xs = self.conv1.forward(xs)?.gelu()?; + let xs = self.conv2.forward(&xs)?.gelu()?; + let mut xs = xs.transpose(1, 2)?; + let (_, seq_len, _) = xs.dims3()?; + let (cos, sin) = self.rotary_emb.forward(0, seq_len, xs.device())?; + for encoder_layer in &self.layers { + xs = encoder_layer.forward(&xs, Some(&cos), Some(&sin), None, false)?; + } + let xs = self.norm.forward(&xs)?; + Ok(xs) + } +} + +pub struct GlmAsrNanoModel { + config: GlmAsrNanoConfig, + audio_tower: GlmAsrEncoder, + multi_modal_projector: TwoLinearMLP, + language_model: LlamaForCausalLM, +} + +impl GlmAsrNanoModel { + pub fn new(vb: VarBuilder, config: GlmAsrNanoConfig) -> Result { + let audio_tower = GlmAsrEncoder::new(vb.pp("audio_tower"), &config.audio_config)?; + let multi_modal_projector = TwoLinearMLP::new( + vb.pp("multi_modal_projector"), + config.audio_config.intermediate_size, + config.text_config.hidden_size * 2, + config.text_config.hidden_size, + config.projector_hidden_act, + true, + "linear_1", + "linear_2", + )?; + let language_model = LlamaForCausalLM::new( + vb.pp("language_model"), + config.text_config.vocab_size, + config.text_config.hidden_size, + config.text_config.num_hidden_layers, + config.text_config.num_attention_heads, + Some(config.text_config.num_key_value_heads), + Some(config.text_config.head_dim), + config.text_config.attention_bias, + "self_attn", + Some("o_proj"), + config.text_config.intermediate_size, + config.text_config.hidden_act, + config.text_config.mlp_bias, + "mlp", + config.text_config.rms_norm_eps, + "input_layernorm", + "post_attention_layernorm", + config.text_config.rope_parameters.rope_theta, + )?; + Ok(Self { + config, + audio_tower, + multi_modal_projector, + language_model, + }) + } + + pub fn get_audio_features( + &self, + input_features: &Tensor, + audio_token_lengths: &[u32], + ) -> Result { + let audio_hidden_states = self.audio_tower.forward(input_features)?; + let bs = audio_hidden_states.dim(0)?; + let audio_hidden_states = + audio_hidden_states.reshape((bs, (), self.config.audio_config.intermediate_size))?; + let audio_embeds = self.multi_modal_projector.forward(&audio_hidden_states)?; + let mut valid_audios = vec![]; + for (i, &len) in audio_token_lengths.iter().enumerate() { + let len = len as usize; + let audio_i = audio_embeds.i((i, 0..len, ..))?; + valid_audios.push(audio_i); + } + let audio_embeds = Tensor::cat(&valid_audios, 0)?; + + Ok(audio_embeds) + } + + pub fn forward( + &mut self, + input_features: Option<&Tensor>, + audio_token_lengths: Option<&Vec>, + input_ids: &Tensor, + seqlen_offset: usize, + ) -> Result { + let mut inputs_embeds = self.language_model.model.embed_tokens.forward(input_ids)?; + if let Some(input_features) = input_features + && let Some(audio_token_len) = audio_token_lengths + { + let audio_token_mask = get_equal_mask(input_ids, self.config.audio_token_id)?; + let audio_embeds = self.get_audio_features(input_features, audio_token_len)?; + inputs_embeds = masked_scatter_dim0(&inputs_embeds, &audio_embeds, &audio_token_mask)?; + } + let logits = self.language_model.forward(&inputs_embeds, seqlen_offset)?; + Ok(logits) + } + pub fn clear_kv_cache(&mut self) { + self.language_model.clear_kv_cache(); + } +} diff --git a/src/models/glm_asr_nano/processor.rs b/src/models/glm_asr_nano/processor.rs index b857573..043a756 100644 --- a/src/models/glm_asr_nano/processor.rs +++ b/src/models/glm_asr_nano/processor.rs @@ -1,30 +1,30 @@ +use std::f32; + use aha_openai_dive::v1::resources::chat::ChatCompletionParameters; use anyhow::Result; -use candle_core::{DType, Device, IndexOp, Tensor}; +use candle_core::{D, DType, Device, IndexOp, Tensor}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::{ models::glm_asr_nano::config::GlmAsrNanoProcessorConfig, - tokenizer::TokenizerModel, - utils::{audio_utils::extract_audios, extract_user_text}, + utils::{ + audio_utils::{create_hann_window, extract_audios, mel_filter_bank, stft_audio}, + tensor_utils::{pad_reflect_last_dim, split_tensor}, + }, }; -pub struct WhisperFeatureExtractor { - feature_size: usize, - sampling_rate: usize, - padding_value: f32, - hop_length: usize, - chunk_length: usize, - n_fft: usize, - dither: f32, -} - pub struct GlmAsrNanoProcessor { sampling_rate: usize, chunk_length: usize, + n_samples: usize, + n_fft: usize, + window: Tensor, + mel_filters: Tensor, + hop_length: usize, audio_token: String, - audio_token_id: u32, + // audio_token_id: u32, max_audio_len: usize, - default_transcription_prompt: String, + // default_transcription_prompt: String, device: Device, } @@ -43,52 +43,193 @@ impl GlmAsrNanoProcessor { let processor_cfg: GlmAsrNanoProcessorConfig = serde_json::from_slice(&std::fs::read(processor_config_path)?)?; let audio_token = processor_cfg.audio_token.clone(); - let audio_token_id = 59260u32; + // let audio_token_id = 59260u32; let max_audio_len = processor_cfg.max_audio_len; - let default_transcription_prompt = processor_cfg.default_transcription_prompt.clone(); + // let default_transcription_prompt = processor_cfg.default_transcription_prompt.clone(); let sampling_rate = processor_cfg.feature_extractor.sampling_rate; let chunk_length = processor_cfg.feature_extractor.chunk_length; + let n_samples = processor_cfg.feature_extractor.n_samples; + let n_fft = processor_cfg.feature_extractor.n_fft; + let hop_length = processor_cfg.feature_extractor.hop_length; + let window = create_hann_window(n_fft, dtype, device)?; + let window = window.unsqueeze(0)?.unsqueeze(0)?; + let mel_filters = mel_filter_bank( + 1 + n_fft / 2, + processor_cfg.feature_extractor.feature_size, + 0.0, + 8000.0, + sampling_rate as f32, + Some("slaney"), + crate::utils::audio_utils::MelScale::Slaney, + false, + device, + )? + .t()?; Ok(Self { sampling_rate, chunk_length, + n_samples, + n_fft, + window, + mel_filters, + hop_length, audio_token, - audio_token_id, + // audio_token_id, max_audio_len, - default_transcription_prompt, + // default_transcription_prompt, device: device.clone(), }) } - // pub fn process_audio(&self, audios: Vec) -> Result { - // let window_size = self.sampling_rate * self.chunk_length; - // let max_windows = self.max_audio_len / self.chunk_length; - // let mut per_sample_windows = vec![]; - // let mut flat_chunks = vec![]; - // for audio_el in audios { - // let n_samples = audio_el.dim(0)?; - // let n_win = ((n_samples + window_size - 1) / window_size).max(1); - // let n_win = if n_win > max_windows { - // max_windows - // } else { - // n_win - // }; - // per_sample_windows.push(n_win); - // let time_cap = (n_win * window_size).min(n_samples); - // for i in 0..n_win { - // let start = i * window_size; - // let end = ((i + 1) * window_size).min(time_cap); - // flat_chunks.push(audio_el.i(start..end)?); - // } - // } - // } + /// 提取音频帧 + pub fn extract_frames(&self, waveform: &Tensor, n_frames: usize) -> Result { + let mut frames = Vec::with_capacity(n_frames); + + for i in 0..n_frames { + let start = i * self.hop_length; + let frame = waveform.narrow(D::Minus1, start, self.n_fft)?; + frames.push(frame); + } + + let result = Tensor::cat(&frames, D::Minus1)?; + let bs = result.dim(0)?; + let reshaped = result.reshape((bs, n_frames, self.n_fft))?; + Ok(reshaped) + } + + pub fn extract_fbank_features(&self, waveform: &Tensor) -> Result { + let pad = self.n_fft / 2; + let waveform = pad_reflect_last_dim(waveform, (pad, pad))?; + let (batch_size, samples) = waveform.dims2()?; + + // 计算输出维度 + let n_frames = (samples - self.n_fft) / self.hop_length + 1; + // (bs, n_frames, n_fft) + let frames = self.extract_frames(&waveform, n_frames)?; + // 应用汉明窗口 + let result = frames.broadcast_mul(&self.window)?; + // 傅立叶变换 + let mut wave_fft = vec![]; + for bs in 0..batch_size { + let wave_i = result.i(bs)?; + let wave_i_vec = wave_i.to_vec2::()?; + let wave_i_fft_vec: Result>> = wave_i_vec + .par_iter() + .map(|frame_wave| stft_audio(self.n_fft, frame_wave)) + .collect(); + let wave_i_fft_vec = wave_i_fft_vec?; + + let wave_i_fft = Tensor::new(wave_i_fft_vec, &self.device)?.unsqueeze(0)?; + wave_fft.push(wave_i_fft); + } + let magnitudes = Tensor::cat(&wave_fft, 0)?.transpose(D::Minus1, D::Minus2)?; + let magnitudes = magnitudes.narrow(D::Minus1, 0, n_frames - 1)?; + let mel_spec = self.mel_filters.broadcast_matmul(&magnitudes)?; + let mel_spec = mel_spec.clamp(1e-10f32, f32::INFINITY)?; + let ln_spec = mel_spec.log()?; + let log10_spec = ln_spec.broadcast_div(&Tensor::new(f32::ln(10.0), mel_spec.device())?)?; + let max_val = log10_spec.max_all()?.affine(1.0, -8.0)?; + let log10_spec = log10_spec.broadcast_maximum(&max_val)?; + let log_spec = log10_spec.affine(1.0, 4.0)?.affine(1.0 / 4.0, 0.0)?; + Ok(log_spec) + } + + pub fn feature_extractor(&self, raw_speech: Vec) -> Result<(Tensor, Tensor)> { + let mut pad_audio = vec![]; + let mut input_features_mask = vec![]; + for audio in raw_speech { + let audio_len = audio.dim(0)?; + let pad_num = self.n_samples - audio_len; + + let audio_pad = audio.pad_with_zeros(0, 0, pad_num)?; + // (n_samples) -> (1, n_samples) + let audio_pad = audio_pad.unsqueeze(0)?; + pad_audio.push(audio_pad); + let mut mask = vec![1u32; audio_len]; + mask.extend_from_slice(&vec![0u32; pad_num]); + input_features_mask.push(mask); + } + let input_features = Tensor::cat(&pad_audio, 0)?; + let input_features_mask = Tensor::new(input_features_mask, input_features.device())?; + let input_features = self.extract_fbank_features(&input_features)?; + let (_, audio_len) = input_features_mask.dims2()?; + let mask_idx: Vec = (0..audio_len) + .step_by(self.hop_length) + .map(|i| i as u32) + .collect(); + let mask_idx = Tensor::new(mask_idx, &self.device)?; + let input_features_mask = input_features_mask.index_select(&mask_idx, D::Minus1)?; + Ok((input_features, input_features_mask)) + } + + pub fn process_audio(&self, audios: Vec) -> Result<(Tensor, Tensor, Vec)> { + let window_size = self.sampling_rate * self.chunk_length; + let max_windows = self.max_audio_len / self.chunk_length; + let mut per_sample_windows = vec![]; + let mut flat_chunks = vec![]; + for audio_el in audios { + let audio_el = if audio_el.rank() == 2 { + audio_el.squeeze(0)? + } else { + audio_el + }; + let n_samples = audio_el.dim(0)?; + let n_win = ((n_samples + window_size - 1) / window_size).max(1); + let n_win = if n_win > max_windows { + max_windows + } else { + n_win + }; + per_sample_windows.push(n_win); + let time_cap = (n_win * window_size).min(n_samples); + for i in 0..n_win { + let start = i * window_size; + let end = ((i + 1) * window_size).min(time_cap); + flat_chunks.push(audio_el.i(start..end)?); + } + } + let (input_features, input_features_mask) = self.feature_extractor(flat_chunks)?; + Ok((input_features, input_features_mask, per_sample_windows)) + } + + pub fn get_audio_token_length(&self, audio_lens: Vec) -> Result> { + let merge_factor = 4; + let audio_lens = audio_lens + .iter() + .map(|i| (i + 2 - 3) + 1) // (pad=1, ks=3, stride=1) + .collect::>() + .iter() + .map(|i| (i + 2 - 3) / 2 + 1) // (pad=1, ks=3, stride=2) + .collect::>(); + let num_tokens = audio_lens + .iter() + .map(|i| (i - merge_factor) / merge_factor + 1) + .collect::>(); + Ok(num_tokens) + } pub fn process_info( &self, - mes: &ChatCompletionParameters - ) -> Result { + mes: &ChatCompletionParameters, + render_text: &str, + ) -> Result<(Tensor, Vec, String)> { let audio_tensors = extract_audios(mes, &self.device, Some(self.sampling_rate))?; - println!("audio: {}", audio_tensors[0]); - // let audio = self.process_audio(audio_tensors)?; - Ok(audio_tensors[0].clone()) + let (input_features, input_features_mask, per_sample_windows) = + self.process_audio(audio_tensors)?; + let audio_lengths = input_features_mask.sum(D::Minus1)?; + let audio_vec = split_tensor(&audio_lengths, &per_sample_windows, 0)?; + let audio_vec: Vec = audio_vec + .iter() + .map(|t| t.sum_all().unwrap().to_scalar::().unwrap()) + .collect(); + + let audio_token_lengths = self.get_audio_token_length(audio_vec)?; + let mut text = render_text.to_string(); + for audio_len in audio_token_lengths.clone() { + let replace = "<|placeholder|>".repeat(audio_len as usize); + text = text.replacen(&self.audio_token, &replace, 1); + } + text = text.replace("<|placeholder|>", &self.audio_token); + Ok((input_features, audio_token_lengths, text)) } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 3423ae2..9e4affb 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod common; pub mod deepseek_ocr; +pub mod glm_asr_nano; pub mod hunyuan_ocr; pub mod minicpm4; pub mod paddleocr_vl; @@ -7,7 +8,6 @@ pub mod qwen2_5vl; pub mod qwen3vl; pub mod rmbg2_0; pub mod voxcpm; -pub mod glm_asr_nano; use aha_openai_dive::v1::resources::chat::{ ChatCompletionChunkResponse, ChatCompletionParameters, ChatCompletionResponse, @@ -17,6 +17,7 @@ use rocket::futures::Stream; use crate::models::{ deepseek_ocr::generate::DeepseekOCRGenerateModel, + glm_asr_nano::generate::GlmAsrNanoGenerateModel, hunyuan_ocr::generate::HunyuanOCRGenerateModel, minicpm4::generate::MiniCPMGenerateModel, paddleocr_vl::generate::PaddleOCRVLGenerateModel, qwen2_5vl::generate::Qwen2_5VLGenerateModel, qwen3vl::generate::Qwen3VLGenerateModel, rmbg2_0::generate::RMBG2_0Model, @@ -51,6 +52,8 @@ pub enum WhichModel { VoxCPM, #[value(name = "voxcpm1.5")] VoxCPM1_5, + #[value(name = "glm-asr-nano-2512")] + GlmASRNano2512, } pub trait GenerateModel { @@ -77,6 +80,7 @@ pub enum ModelInstance<'a> { PaddleOCRVL(Box>), RMBG2_0(Box), VoxCPM(Box), + GlmASRNano(GlmAsrNanoGenerateModel<'a>), } impl<'a> GenerateModel for ModelInstance<'a> { @@ -90,6 +94,7 @@ impl<'a> GenerateModel for ModelInstance<'a> { ModelInstance::PaddleOCRVL(model) => model.generate(mes), ModelInstance::RMBG2_0(model) => model.generate(mes), ModelInstance::VoxCPM(model) => model.generate(mes), + ModelInstance::GlmASRNano(model) => model.generate(mes), } } @@ -113,6 +118,7 @@ impl<'a> GenerateModel for ModelInstance<'a> { ModelInstance::PaddleOCRVL(model) => model.generate_stream(mes), ModelInstance::RMBG2_0(model) => model.generate_stream(mes), ModelInstance::VoxCPM(model) => model.generate_stream(mes), + ModelInstance::GlmASRNano(model) => model.generate_stream(mes), } } } @@ -171,6 +177,10 @@ pub fn load_model(model_type: WhichModel, path: &str) -> Result { + let model = GlmAsrNanoGenerateModel::init(path, None, None)?; + ModelInstance::GlmASRNano(model) + } }; Ok(model) } diff --git a/src/models/qwen3vl/model.rs b/src/models/qwen3vl/model.rs index 8872893..f0ec97a 100644 --- a/src/models/qwen3vl/model.rs +++ b/src/models/qwen3vl/model.rs @@ -205,6 +205,7 @@ impl Qwen3VLVisionBlock { vb.pp("mlp"), config.hidden_size, config.intermediate_size, + config.hidden_size, config.hidden_act, true, "linear_fc1", diff --git a/src/models/rmbg2_0/model.rs b/src/models/rmbg2_0/model.rs index af74a18..9417750 100644 --- a/src/models/rmbg2_0/model.rs +++ b/src/models/rmbg2_0/model.rs @@ -255,7 +255,7 @@ impl SwinTransformerBlock { )?; let norm2 = get_layer_norm(vb.pp("norm2"), 1e-5, dim)?; let mlp_dim = (dim as f32 * mlp_ratio) as usize; - let mlp = TwoLinearMLP::new(vb.pp("mlp"), dim, mlp_dim, act, true, "fc1", "fc2")?; + let mlp = TwoLinearMLP::new(vb.pp("mlp"), dim, mlp_dim, dim, act, true, "fc1", "fc2")?; Ok(Self { norm1, attn, diff --git a/src/models/voxcpm/model.rs b/src/models/voxcpm/model.rs index 79da2ef..eaacad3 100644 --- a/src/models/voxcpm/model.rs +++ b/src/models/voxcpm/model.rs @@ -728,11 +728,8 @@ impl VoxCPMModel { ) -> Result> { let text_token = self.tokenizer.encode(prompt_text)?; let text_token = Tensor::from_slice(&text_token, text_token.len(), &self.device)?; - let mut audio = load_audio_with_resample( - &prompt_wav_path, - &self.device, - Some(self.sample_rate), - )?; + let mut audio = + load_audio_with_resample(&prompt_wav_path, &self.device, Some(self.sample_rate))?; let patch_len = self.patch_size * self.chunk_size; if audio.dim(1)? % patch_len != 0 { audio = audio.pad_with_zeros(D::Minus1, 0, patch_len - audio.dim(1)? % patch_len)?; diff --git a/src/position_embed/rope.rs b/src/position_embed/rope.rs index 2189059..8853489 100644 --- a/src/position_embed/rope.rs +++ b/src/position_embed/rope.rs @@ -115,6 +115,49 @@ pub fn apply_rotary_pos_emb( Ok((q_embed, k_embed)) } +pub fn glm_asr_apply_rotary_pos_emb( + q: &Tensor, + k: &Tensor, + cos: &Tensor, + sin: &Tensor, + tof32: bool, +) -> Result<(Tensor, Tensor)> { + // sin/cos: to (bs, 1, seq_len, head_dim/2) + // q/k: (bs, n_head, seq_len, head_dim) + let mut cos = cos.clone(); + let mut sin = sin.clone(); + if cos.rank() == 2 { + // (seq_len, head_dim/2) -> (1, 1, seq_len, head_dim/2) + cos = cos.unsqueeze(0)?.unsqueeze(0)?; + sin = sin.unsqueeze(0)?.unsqueeze(0)?; + } + if cos.rank() == 3 { + // (bs, seq_len, head_dim/2) -> (bs, 1, seq_len, head_dim/2) + cos = cos.unsqueeze(1)?; + sin = sin.unsqueeze(1)?; + } + let orig_dtype = q.dtype(); + let q = if tof32 { &q.to_dtype(DType::F32)? } else { q }; + let k = if tof32 { &k.to_dtype(DType::F32)? } else { k }; + let cos = cos.to_dtype(q.dtype())?; + let sin = sin.to_dtype(q.dtype())?; + let rotary_dim = cos.dim(D::Minus1)?; + let q_rot = q.narrow(D::Minus1, 0, rotary_dim)?; + let q_pass = q.narrow(D::Minus1, rotary_dim, rotary_dim)?; + let k_rot = k.narrow(D::Minus1, 0, rotary_dim)?; + let k_pass = k.narrow(D::Minus1, rotary_dim, rotary_dim)?; + + let q_embed = q_rot + .broadcast_mul(&cos)? + .add(&rotate_half(&q_rot)?.broadcast_mul(&sin)?)?; + let k_embed = k_rot + .broadcast_mul(&cos)? + .add(&rotate_half(&k_rot)?.broadcast_mul(&sin)?)?; + let q_embed = Tensor::cat(&[q_embed, q_pass], D::Minus1)?.to_dtype(orig_dtype)?; + let k_embed = Tensor::cat(&[k_embed, k_pass], D::Minus1)?.to_dtype(orig_dtype)?; + Ok((q_embed, k_embed)) +} + #[derive(Debug, Clone)] pub struct Qwen2_5VLTextRotaryEmbedding { inv_freq: Vec, diff --git a/src/utils/audio_utils.rs b/src/utils/audio_utils.rs index 8a17778..efb5de1 100644 --- a/src/utils/audio_utils.rs +++ b/src/utils/audio_utils.rs @@ -10,13 +10,28 @@ use aha_openai_dive::v1::resources::chat::{ use anyhow::{Result, anyhow}; use base64::Engine; use base64::prelude::BASE64_STANDARD; -use candle_core::{D, Device, Tensor}; +use candle_core::{D, DType, Device, IndexOp, Tensor}; use candle_nn::{Conv1d, Conv1dConfig, Module}; +#[cfg(feature = "ffmpeg")] +use ffmpeg_next as ffmpeg; use hound::{SampleFormat, WavReader}; use num::integer::gcd; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use realfft::RealFftPlanner; +use symphonia::core::audio::{AudioBufferRef, Signal}; +use symphonia::core::codecs::DecoderOptions; +use symphonia::core::formats::FormatOptions; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::meta::MetadataOptions; +use symphonia::core::probe::Hint; +// use rubato::{ +// Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, SincInterpolationType, +// WindowFunction, +// }; +// use audioadapter_buffers::direct::InterleavedSlice; use crate::utils::get_default_save_dir; +use crate::utils::tensor_utils::linspace; // 重采样方法枚举 #[derive(Debug, Clone, Copy)] @@ -224,6 +239,7 @@ pub fn resample_simple(waveform: &Tensor, orig_freq: i64, new_freq: i64) -> Resu None, ) } + pub fn load_audio_from_url(url: &str) -> Result { tokio::task::block_in_place(|| { let client = reqwest::blocking::Client::new(); @@ -236,7 +252,13 @@ pub fn load_audio_from_url(url: &str) -> Result { } let temp_dir = get_default_save_dir().expect("Failed to get home directory"); let temp_dir = PathBuf::from(temp_dir); - let temp_path = temp_dir.join("temp_audio.wav"); + let temp_path = if url.contains("wav") { + temp_dir.join("temp_audio.wav") + } else if url.contains("mp3") { + temp_dir.join("temp_audio.mp3") + } else { + return Err(anyhow::anyhow!("load audio only surpport wav/mp3 format")); + }; let mut file = std::fs::File::create(&temp_path)?; let mut content = Cursor::new(response.bytes()?); @@ -266,10 +288,19 @@ pub fn get_audio_path(path_str: &str) -> Result { Ok(path) } else if path_str.starts_with("data:audio") && path_str.contains("base64,") { let data: Vec<&str> = path_str.split("base64,").collect(); + let file_mes = data[0]; let data = data[1]; let temp_dir = get_default_save_dir().expect("Failed to get home directory"); let temp_dir = PathBuf::from(temp_dir); - let temp_path = temp_dir.join("temp_audio.wav"); + let temp_path = if file_mes.contains("wav") { + temp_dir.join("temp_audio.wav") + } else if file_mes.contains("mpeg") { + temp_dir.join("temp_audio.mp3") + } else { + return Err(anyhow::anyhow!( + "base64 audio only surpport wav/mpeg(mp3) format" + )); + }; save_audio_from_base64(data, &temp_path)?; Ok(temp_path) } else { @@ -277,8 +308,58 @@ pub fn get_audio_path(path_str: &str) -> Result { } } -pub fn load_audio(path: &str, device: &Device) -> Result<(Tensor, usize)> { +pub fn load_audio_mono_vec(path: &str) -> Result<(Vec, usize)> { let audio_path = get_audio_path(path)?; + let mut reader = WavReader::open(audio_path)?; + let spec = reader.spec(); + let samples: Vec = match spec.sample_format { + SampleFormat::Int => { + // 将整数样本转换为浮点数 [-1.0, 1.0] + // println!("spec.bits_per_sample: {}", spec.bits_per_sample); + match spec.bits_per_sample { + 8 => reader + .samples::() + .map(|s| s.map(|sample| sample as f32 / i8::MAX as f32)) + .collect::, _>>()?, + 16 => reader + .samples::() + .map(|s| s.map(|sample| sample as f32 / i16::MAX as f32)) + .collect::, _>>()?, + 24 => reader + .samples::() + .map(|s| s.map(|sample| sample as f32 / 8388607.0)) + .collect::, _>>()?, + _ => { + return Err(anyhow::anyhow!( + "Unsupported bit depth: {}", + spec.bits_per_sample + )); + } + } + } + SampleFormat::Float => { + // 直接读取浮点数样本 + reader.samples::().collect::, _>>()? + } + }; + let mono_samples = if spec.channels == 2 { + let mut mono = Vec::with_capacity(samples.len() / 2); + for chunk in samples.chunks(2) { + if chunk.len() == 2 { + mono.push((chunk[0] + chunk[1]) / 2.0); + } + } + mono + } else if spec.channels == 1 { + samples + } else { + return Err(anyhow::anyhow!("only supported mono or stereo")); + }; + let sample_rate = spec.sample_rate as usize; + Ok((mono_samples, sample_rate)) +} + +pub fn load_audio_use_hound(audio_path: PathBuf, device: &Device) -> Result<(Tensor, usize)> { let mut reader = WavReader::open(audio_path)?; let spec = reader.spec(); let samples: Vec = match spec.sample_format { @@ -321,6 +402,7 @@ pub fn load_audio(path: &str, device: &Device) -> Result<(Tensor, usize)> { device, )? .t()?; + // println!("audio channels: {}", spec.channels); if spec.channels > 1 { // 对channel通道求平均, channel维度变为1 audio_tensor = audio_tensor.mean_keepdim(0)?; @@ -328,12 +410,109 @@ pub fn load_audio(path: &str, device: &Device) -> Result<(Tensor, usize)> { Ok((audio_tensor, sample_rate as usize)) } +pub fn load_audio_use_symphonia(path: PathBuf, device: &Device) -> Result<(Tensor, usize)> { + let file = File::open(path.clone())?; + let mss = MediaSourceStream::new(Box::new(file), Default::default()); + + let mut hint = Hint::new(); + let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); + hint.with_extension(extension); + + let probed = symphonia::default::get_probe().format( + &hint, + mss, + &FormatOptions::default(), + &MetadataOptions::default(), + )?; + + let mut format = probed.format; + let track = format + .default_track() + .ok_or("No default track found") + .map_err(|e| anyhow!("symphonia read err: {}", e))?; + let mut channels = 1; + let sample_rate = track.codec_params.sample_rate.unwrap_or(0); + // 创建解码器 + let mut decoder = + symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?; + + // 用于存储所有音频样本的缓冲区 + let mut all_samples: Vec> = Vec::new(); + + // 循环读取数据包并解码 + while let Ok(packet) = format.next_packet() { + match decoder.decode(&packet) { + Ok(decoded) => { + match decoded { + AudioBufferRef::F32(buf) => { + channels = buf.spec().channels.count(); + // 对于浮点格式 + for channel in 0..channels { + if all_samples.len() <= channel { + all_samples.push(Vec::new()); + } + let channel_data = buf.chan(channel); + all_samples[channel].extend_from_slice(channel_data); + } + } + AudioBufferRef::S16(buf) => { + channels = buf.spec().channels.count(); + // 对于16位整数格式,转换为f32 + for channel in 0..channels { + if all_samples.len() <= channel { + all_samples.push(Vec::new()); + } + let channel_data = buf.chan(channel); + let float_samples: Vec = channel_data + .iter() + .map(|&s| s as f32 / 32768.0) // 转换为[-1, 1] + .collect(); + all_samples[channel].extend(float_samples); + } + } + AudioBufferRef::S24(buf) => { + channels = buf.spec().channels.count(); + // 处理24位音频 + for channel in 0..channels { + if all_samples.len() <= channel { + all_samples.push(Vec::new()); + } + let channel_data = buf.chan(channel); + let float_samples: Vec = channel_data + .iter() + .map(|&s| s.inner() as f32 / 8388608.0) // 转换为[-1, 1] + .collect(); + all_samples[channel].extend(float_samples); + } + } + _ => { + println!("不支持的音频格式"); + } + } + } + Err(e) => { + eprintln!("解码错误: {}", e); + break; + } + } + } + let mut audio_tensor = Tensor::new(all_samples, device)?; + if channels > 1 { + // 对channel通道求平均, channel维度变为1 + audio_tensor = audio_tensor.mean_keepdim(0)?; + } + Ok((audio_tensor, sample_rate as usize)) +} + pub fn load_audio_with_resample( path: &str, device: &Device, target_sample_rate: Option, ) -> Result { - let (mut audio, sr) = load_audio(path, device)?; + let audio_path = get_audio_path(path)?; + // hound 只支持wav文件 + // let (mut audio, sr) = load_audio_use_hound(audio_path, device)?; + let (mut audio, sr) = load_audio_use_symphonia(audio_path, device)?; if let Some(target_sample_rate) = target_sample_rate && target_sample_rate != sr { @@ -405,10 +584,33 @@ pub fn extract_audio_url(mes: &ChatCompletionParameters) -> Vec { audio_vec } -pub fn extract_audios(mes: &ChatCompletionParameters, device: &Device, target_sample_rate: Option) -> Result> { +pub fn extract_audios( + mes: &ChatCompletionParameters, + device: &Device, + target_sample_rate: Option, +) -> Result> { let audio_url_vec = extract_audio_url(mes); // 并行加载音频 - audio_url_vec.par_iter().map(|url| load_audio_with_resample(url, device, target_sample_rate)).collect() + audio_url_vec + .par_iter() + .map(|url| load_audio_with_resample(url, device, target_sample_rate)) + .collect() + // #[cfg(not(feature = "ffmpeg"))] + // { + // audio_url_vec + // .par_iter() + // .map(|url| load_audio_with_resample(url, device, target_sample_rate)) + // .collect() + // } + // #[cfg(feature = "ffmpeg")] + // { + // // 该方法wav文件解析有问题 + // use crate::utils::audio_utils::load_and_resample_audio_ffmpeg; + // audio_url_vec + // .par_iter() + // .map(|url| load_and_resample_audio_ffmpeg(url, target_sample_rate, device)) + // .collect() + // } } // 从 ChatCompletionResponse 中提取音频数据 @@ -468,3 +670,369 @@ pub fn extract_and_save_audio_from_response( Ok(saved_files) } + +#[cfg(feature = "ffmpeg")] +pub fn load_and_resample_audio_ffmpeg( + file_path: &str, + target_sample_rate: Option, + device: &Device, +) -> Result { + // 方法只支持mp3 + // wav文件会报错: + // [SWR @ 0x745ff0037840] Input channel layout "" is invalid or unsupported. + // Error: Invalid argument + // 未解决 + ffmpeg::init().map_err(|e| anyhow!(format!("Failed to initialize ffmpeg: {}", e)))?; + + // 打开文件 + let mut ictx = ffmpeg::format::input(&Path::new(file_path)) + .map_err(|e| anyhow!(format!("Failed to open audio file: {}", e)))?; + + // 找到音频流 + let stream = ictx + .streams() + .best(ffmpeg::media::Type::Audio) + .ok_or_else(|| anyhow!(format!("No audio stream found")))?; + let stream_index = stream.index(); + + // 获取解码器 + let codec_params = stream.parameters(); + let mut decoder = ffmpeg::codec::context::Context::from_parameters(codec_params) + .map_err(|e| anyhow!(format!("无法创建解码器上下文: {}", e)))? + .decoder() + .audio() + .map_err(|e| anyhow!(format!("不是音频解码器: {}", e)))?; + + // // 直接更改输入的channel_layout也会报错:Error: Input changed + // let src_channels = decoder.channels(); + // let layout = decoder.channel_layout(); + // if layout.is_empty() || layout.channels() == 0 { + // // 如果没有有效的 channel layout,使用基于通道数的默认布局 + // let layout = ffmpeg::channel_layout::ChannelLayout::default(src_channels as i32); + // decoder.set_channel_layout(layout); + // } + let original_sample_rate = decoder.rate() as usize; + let needs_resampling = match target_sample_rate { + None => false, + Some(target_sr) => target_sr != original_sample_rate, + }; + // 存储音频数据 + let mut audio_buffer = vec![]; + if !needs_resampling { + // 不需要重采样,直接解码音频 + for (stream, packet) in ictx.packets() { + if stream.index() == stream_index { + decoder.send_packet(&packet)?; + let mut decoded = ffmpeg::util::frame::Audio::empty(); + while decoder.receive_frame(&mut decoded).is_ok() { + let planes = decoded.planes(); + if planes == 1 { + let data_slice = decoded.plane::(0); + audio_buffer.extend_from_slice(data_slice); + } else { + let mut channel_data: Vec<&[f32]> = vec![]; + for plane_idx in 0..planes { + let plane_data = decoded.plane::(plane_idx); + channel_data.push(plane_data); + } + let channel_len = channel_data[0].len(); + for sample_idx in 0..channel_len { + let mut sum = 0.0f32; + for channel in &channel_data { + sum += channel[sample_idx]; + } + let avg = sum / planes as f32; + audio_buffer.push(avg); + } + } + } + } + } + } else { + let target_sample_rate = target_sample_rate.unwrap_or(16000); + // 创建重采样器, 通道为1 + let mut resampler = ffmpeg::software::resampling::context::Context::get( + decoder.format(), + decoder.channel_layout(), + decoder.rate() as u32, + ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), + ffmpeg::channel_layout::ChannelLayout::default(1), + target_sample_rate as u32, + ) + .map_err(|e| anyhow!(format!("无法创建重采样器: {}", e)))?; + + // let mut resampler = decoder.resampler( + // ffmpeg::format::Sample::F32(ffmpeg::format::sample::Type::Planar), + // ffmpeg::channel_layout::ChannelLayout::default(target_channels as i32), + // target_sample_rate, + // )?; + + // 处理所有包 + for (stream, packet) in ictx.packets() { + if stream.index() == stream_index { + // 解码 + decoder.send_packet(&packet)?; + + let mut decoded = ffmpeg::util::frame::Audio::empty(); + while decoder.receive_frame(&mut decoded).is_ok() { + // 重采样 + let mut resampled = ffmpeg::util::frame::Audio::empty(); + resampler.run(&decoded, &mut resampled)?; + + // 提取数据,Planar格式 + let data_slice = resampled.plane::(0); + audio_buffer.extend_from_slice(data_slice); + } + } + } + + // 处理剩余数据 + decoder.send_eof()?; + + let mut decoded = ffmpeg::util::frame::Audio::empty(); + while decoder.receive_frame(&mut decoded).is_ok() { + let mut resampled = ffmpeg::util::frame::Audio::empty(); + resampler.run(&decoded, &mut resampled)?; + + // 提取数据,Planar格式 + let data_slice = resampled.plane::(0); + audio_buffer.extend_from_slice(data_slice); + } + } + + let audio_tensor = Tensor::new(audio_buffer, device)?; + + Ok(audio_tensor) +} + +// pub fn load_and_resample_audio_rubato( +// file_path: &str, +// target_sample_rate: usize, +// device: &Device, +// ) -> Result { +// let (mono_audio, ori_sample_rate) = load_audio_mono_vec(file_path)?; +// let params = SincInterpolationParameters { +// sinc_len: 256, +// f_cutoff: 0.95, +// interpolation: SincInterpolationType::Cubic, +// oversampling_factor: 256, +// window: WindowFunction::BlackmanHarris2, +// }; +// let input_len = mono_audio.len(); +// let mut resampler = Async::::new_sinc( +// target_sample_rate as f64 / ori_sample_rate as f64, // 重采样比例 +// 1.0, // 输出/输入采样率比 +// ¶ms, +// input_len, +// 1, // 单通道 +// FixedAsync::Input, +// ) +// .map_err(|e| anyhow!(format!("无法创建重采样器: {}", e)))?; + +// let mono_audio: Vec = mono_audio.iter().map(|x| *x as f64).collect(); +// let input_adapter = InterleavedSlice::new(&mono_audio, 1, input_len)?; + +// let mut outdata = vec![0.0f64; input_len * 2]; +// let mut output_adapter = InterleavedSlice::new_mut(&mut outdata, 1, input_len * 2)?; +// // Preparations +// let mut indexing = Indexing { +// input_offset: 0, +// output_offset: 0, +// active_channels_mask: None, +// partial_len: None, +// }; +// let mut input_frames_left = input_len; +// let mut input_frames_next = resampler.input_frames_max(); +// while input_frames_left >= input_frames_next { +// let (frames_read, frames_written) = +// resampler.process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing))?; +// indexing.input_offset += frames_read; +// indexing.output_offset += frames_written; +// input_frames_left -= frames_read; +// input_frames_next = resampler.input_frames_next(); +// } +// indexing.partial_len = Some(input_frames_left); +// let (_nbr_in, _nbr_out) = resampler +// .process_into_buffer(&input_adapter, &mut output_adapter, Some(&indexing)) +// .unwrap(); +// let output_len = input_len * target_sample_rate / ori_sample_rate; +// let audio_tensor = +// Tensor::new(&outdata[0..output_len], device)?.to_dtype(candle_core::DType::F32)?; +// Ok(audio_tensor) +// } + +pub fn create_hann_window(window_size: usize, dtype: DType, device: &Device) -> Result { + let n = window_size as f64; + let window: Vec = (0..window_size) + .map(|i| { + let i_f64 = i as f64; + let val = 0.5 * (1.0 - (2.0 * PI * i_f64 / n).cos()); + val as f32 + }) + .collect(); + Ok(Tensor::from_vec(window, window_size, device)?.to_dtype(dtype)?) +} + +/// 梅尔频率刻度类型 +#[derive(Debug, Clone, Copy)] +pub enum MelScale { + Htk, + Kaldi, + Slaney, +} + +/// 将赫兹转换为梅尔频率 +pub fn hertz_to_mel(freq: f32, mel_scale: MelScale) -> f32 { + match mel_scale { + MelScale::Htk => 2595.0 * ((1.0 + freq / 700.0).log10()), + MelScale::Kaldi => 1127.0 * ((1.0 + freq / 700.0).ln()), + MelScale::Slaney => { + let min_log_hertz = 1000.0; + let min_log_mel = 15.0; + let logstep = 27.0 / 6.4_f32.ln(); + let mut mels = 3.0 * freq / 200.0; + + if freq >= min_log_hertz { + mels = min_log_mel + (freq / min_log_hertz).ln() * logstep; + } + mels + } + } +} + +/// 将梅尔频率转换为赫兹 +pub fn mel_to_hertz(mels: f32, mel_scale: MelScale) -> f32 { + match mel_scale { + MelScale::Htk => 700.0 * (10.0_f32.powf(mels / 2595.0) - 1.0), + MelScale::Kaldi => 700.0 * (f32::exp(mels / 1127.0) - 1.0), + MelScale::Slaney => { + let min_log_hertz = 1000.0; + let min_log_mel = 15.0; + let logstep = 6.4_f32.ln() / 27.0; + let mut freq = 200.0 * mels / 3.0; + + if mels >= min_log_mel { + freq = min_log_hertz * f32::exp(logstep * (mels - min_log_mel)); + } + freq + } + } +} + +pub fn create_triangular_filter_bank(fft_freqs: &Tensor, filter_freqs: &Tensor) -> Result { + // fft_freqs/filter_freqs -> 1d + let len = filter_freqs.dim(0)?; + let filter_diff = filter_freqs + .narrow(0, 1, len - 1)? + .sub(&filter_freqs.narrow(0, 0, len - 1)?)?; + let slopes = filter_freqs + .unsqueeze(0)? + .broadcast_sub(&fft_freqs.unsqueeze(1)?)?; + let down_slopes = slopes + .narrow(D::Minus1, 0, len - 2)? + .affine(-1.0, 0.0)? + .broadcast_div(&filter_diff.narrow(0, 0, len - 2)?)?; + let up_slopes = slopes + .narrow(D::Minus1, 2, len - 2)? + .broadcast_div(&filter_diff.narrow(0, 1, len - 2)?)?; + let res = down_slopes + .minimum(&up_slopes)? + .maximum(&Tensor::zeros_like(&down_slopes)?)?; + Ok(res) +} + +/// 创建梅尔滤波器组 +pub fn mel_filter_bank( + num_frequency_bins: usize, + num_mel_filters: usize, + min_frequency: f32, + max_frequency: f32, + sampling_rate: f32, + norm: Option<&str>, + mel_scale: MelScale, + triangularize_in_mel_space: bool, + device: &Device, +) -> Result { + // 参数验证 + if let Some(n) = norm + && n != "slaney" + { + return Err(anyhow::anyhow!("norm must be one of None or 'slaney'")); + } + if num_frequency_bins < 2 { + return Err(anyhow::anyhow!( + "Require num_frequency_bins: {} >= 2", + num_frequency_bins + )); + } + if min_frequency > max_frequency { + return Err(anyhow::anyhow!( + "Require min_frequency: {} <= max_frequency: {}", + min_frequency, + max_frequency + )); + } + // 计算梅尔频率范围 + let mel_min = hertz_to_mel(min_frequency, mel_scale); + let mel_max = hertz_to_mel(max_frequency, mel_scale); + + // 在梅尔刻度上均匀分布频率点(包括边界点) + let mel_freqs = linspace(mel_min, mel_max, num_mel_filters + 2, device)?; + + // 将梅尔频率转换回赫兹频率 + let filter_freqs: Vec = mel_freqs + .to_vec1::()? + .iter() + .map(|&m| mel_to_hertz(m, mel_scale)) + .collect(); + let mut filter_freqs = Tensor::new(filter_freqs, device)?; + + let fft_freqs = if triangularize_in_mel_space { + // 在梅尔空间中应用三角滤波器 + let fft_bin_width = sampling_rate / ((num_frequency_bins as f32 - 1.0) * 2.0); + let fft_vec: Vec = (0..num_frequency_bins) + .map(|i| hertz_to_mel(fft_bin_width * i as f32, mel_scale)) + .collect(); + filter_freqs = mel_freqs; + Tensor::new(fft_vec, device)? + } else { + // 在赫兹频率上 + linspace(0.0, sampling_rate / 2.0, num_frequency_bins, device)? + }; + + // 创建三角滤波器组 + let mut mel_filters = create_triangular_filter_bank(&fft_freqs, &filter_freqs)?; + + // 如果需要,进行归一化 + if let Some(n) = norm + && n == "slaney" + { + // Slaney风格的归一化 + let enorm = (2.0 + / filter_freqs + .i(2..num_mel_filters + 2)? + .sub(&filter_freqs.i(0..num_mel_filters)?)?)? + .unsqueeze(0)?; + mel_filters = mel_filters.broadcast_mul(&enorm)?; + } + + // // 检查是否有零值滤波器 + // let mel_max = mel_filters.max(0)?; + // let mel_max_eq_zero = mel_max.eq(&Tensor::zeros_like(&mel_max)?)?; + // let eq_zero_index = zero_index_vec(&mel_max_eq_zero)?; + // if eq_zero_index.len() > 0 { + // println!("At least one mel filter has all zero values."); + // } + + Ok(mel_filters) +} + +pub fn stft_audio(n_fft: usize, frame_wave: &[f32]) -> Result> { + let mut real_planner = RealFftPlanner::::new(); + let r2c = real_planner.plan_fft_forward(n_fft); + let mut spectrum = r2c.make_output_vec(); + let mut frame_wave = frame_wave.to_owned(); + r2c.process(&mut frame_wave, &mut spectrum)?; + let output: Vec = spectrum.iter().map(|complex| complex.norm_sqr()).collect(); + Ok(output) +} diff --git a/src/utils/tensor_utils.rs b/src/utils/tensor_utils.rs index fbee88d..2ee4a2b 100644 --- a/src/utils/tensor_utils.rs +++ b/src/utils/tensor_utils.rs @@ -844,3 +844,29 @@ pub fn nonzero(input: &Tensor) -> Result<(Vec, Vec)> { } Ok((topk_ids, token_ids_all)) } + +pub fn pad_reflect_last_dim(t: &Tensor, pad: (usize, usize)) -> Result { + let (pad_l, pad_r) = pad; + let last_dim = t.dim(D::Minus1)?; + if pad_l >= last_dim || pad_r >= last_dim { + return Err(anyhow!(format!( + "input pad_l {}, pad_r {} must less than t last_dim: {}", + pad_l, pad_r, last_dim + ))); + } + let mut pad_tensor = t.clone(); + if pad_l > 0 { + let left = pad_tensor.narrow(D::Minus1, 1, pad_l)?.contiguous()?; + let last_dim_id = left.rank() - 1; + let left_flip = left.flip(&[last_dim_id])?; + pad_tensor = Tensor::cat(&[&left_flip, &pad_tensor], D::Minus1)?; + } + if pad_r > 0 { + let start_i = last_dim - pad_r; + let right = pad_tensor.narrow(D::Minus1, start_i, pad_r)?.contiguous()?; + let last_dim_id = right.rank() - 1; + let right_flip = right.flip(&[last_dim_id])?; + pad_tensor = Tensor::cat(&[&pad_tensor, &right_flip], D::Minus1)?; + } + Ok(pad_tensor) +} diff --git a/tests/messy_test.rs b/tests/messy_test.rs index 066c450..e0d083f 100644 --- a/tests/messy_test.rs +++ b/tests/messy_test.rs @@ -1,13 +1,31 @@ +use aha::utils::audio_utils::create_hann_window; use anyhow::Result; -use candle_core::Tensor; +use candle_core::DType; #[test] fn messy_test() -> Result<()> { - // RUST_BACKTRACE=1 cargo test -F cuda messy_test -r -- --nocapture + // RUST_BACKTRACE=1 cargo test -F cuda,ffmpeg messy_test -r -- --nocapture let device = &candle_core::Device::Cpu; - // let path = get_default_save_dir(); - let x = Tensor::arange(0.0, 9.0, device)?; - println!("x: {}", x); + let window = create_hann_window(400, DType::F32, device)?; + println!("window: {}", window); + // let audio_path = "file:///home/jhq/Videos/voice_01.wav"; + // let audio_path = "/home/jhq/Videos/zh.mp3"; + // let audio_path = "/home/jhq/Videos/zh.mp3"; + // // let audio_tensor = load_and_resample_audio_rubato(audio_path, 16000, device)?; + // // let audio_tensor = load_audio_with_resample(audio_path, device, Some(16000))?; + // // println!("audio_tensor: {}", audio_tensor); + // #[cfg(feature = "ffmpeg")] + // { + // use aha::utils::audio_utils::load_and_resample_audio_ffmpeg; + + // let audio_tensor = load_and_resample_audio_ffmpeg(audio_path, Some(16000), device)?; + // println!("audio_tensor: {}", audio_tensor); + // } + + // // let path = get_default_save_dir(); + // // let x = Tensor::new(array, device) + // let x = Tensor::arange(0.0, 9.0, device)?; + // println!("x: {}", x); // let x = x // .unsqueeze(0)? // .unsqueeze(0)? diff --git a/tests/test_glm_asr_nano.rs b/tests/test_glm_asr_nano.rs index 0df6e9e..7414cb5 100644 --- a/tests/test_glm_asr_nano.rs +++ b/tests/test_glm_asr_nano.rs @@ -1,11 +1,14 @@ -use aha::models::glm_asr_nano::generate::GlmAsrNanoGenerateModel; +use std::{pin::pin, time::Instant}; + +use aha::models::{GenerateModel, glm_asr_nano::generate::GlmAsrNanoGenerateModel}; use aha_openai_dive::v1::resources::chat::ChatCompletionParameters; -use anyhow::{Result}; +use anyhow::Result; +use rocket::futures::StreamExt; #[test] fn glm_asr_nano_generate() -> Result<()> { // RUST_BACKTRACE=1 cargo test -F cuda glm_asr_nano_generate -r -- --nocapture - let model_path = "/home/jhq/huggingface_model/zai-org/GLM-ASR-Nano-2512/"; + let model_path = "/home/jhq/huggingface_model/ZhipuAI/GLM-ASR-Nano-2512/"; let message = r#" { "model": "glm-asr-nano", @@ -17,9 +20,9 @@ fn glm_asr_nano_generate() -> Result<()> { "type": "audio", "audio_url": { - "url": "file://./assets/audio/voice_01.wav" + "url": "https://sis-sample-audio.obs.cn-north-1.myhuaweicloud.com/16k16bit.mp3" } - }, + }, { "type": "text", "text": "Please transcribe this audio into text" @@ -30,7 +33,62 @@ fn glm_asr_nano_generate() -> Result<()> { } "#; let mes: ChatCompletionParameters = serde_json::from_str(message)?; - let glm_asr_model = GlmAsrNanoGenerateModel::init(model_path, None, None)?; - let _ = glm_asr_model.generate(mes)?; + let i_start = Instant::now(); + let mut glm_asr_model = GlmAsrNanoGenerateModel::init(model_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + let i_start = Instant::now(); + let res = glm_asr_model.generate(mes)?; + let i_duration = i_start.elapsed(); + println!("generate: \n {:?}", res); + if res.usage.is_some() { + let num_token = res.usage.as_ref().unwrap().total_tokens; + let duration_secs = i_duration.as_secs_f64(); + let tps = num_token as f64 / duration_secs; + println!("Tokens per second (TPS): {:.2}", tps); + } + println!("Time elapsed in generate is: {:?}", i_duration); Ok(()) -} \ No newline at end of file +} + +#[tokio::test] +async fn glm_asr_nano_stream() -> Result<()> { + // RUST_BACKTRACE=1 cargo test -F cuda glm_asr_nano_stream -r -- --nocapture + let model_path = "/home/jhq/huggingface_model/ZhipuAI/GLM-ASR-Nano-2512/"; + let message = r#" + { + "model": "glm-asr-nano", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "audio", + "audio_url": + { + "url": "file://./assets/audio/zh.mp3" + } + }, + { + "type": "text", + "text": "Please transcribe this audio into text" + } + ] + } + ] + } + "#; + let mes: ChatCompletionParameters = serde_json::from_str(message)?; + let i_start = Instant::now(); + let mut glm_asr_model = GlmAsrNanoGenerateModel::init(model_path, None, None)?; + let i_duration = i_start.elapsed(); + println!("Time elapsed in load model is: {:?}", i_duration); + let i_start = Instant::now(); + let mut stream = pin!(glm_asr_model.generate_stream(mes)?); + while let Some(item) = stream.next().await { + println!("generate: \n {:?}", item); + } + let i_duration = i_start.elapsed(); + println!("Time elapsed in generate is: {:?}", i_duration); + Ok(()) +} diff --git a/tests/weight_test.rs b/tests/weight_test.rs index ab68db6..bbfa740 100644 --- a/tests/weight_test.rs +++ b/tests/weight_test.rs @@ -119,3 +119,22 @@ fn hunyuanocr_weight() -> Result<()> { println!("model_list: {:?}", model_list); Ok(()) } + +#[test] +fn glm_asr_nano_weight() -> Result<()> { + let model_path = "/home/jhq/huggingface_model/ZhipuAI/GLM-ASR-Nano-2512/"; + let model_list = find_type_files(model_path, "safetensors")?; + + let device = Device::Cpu; + for m in &model_list { + let weights = safetensors::load(m, &device)?; + for (key, tensor) in weights.iter() { + if key.contains(".embed_tokens") { + println!("=== {} === {:?}", key, tensor.shape()); + } + // println!("=== {} === {:?}", key, tensor.shape()); + } + } + println!("model_list: {:?}", model_list); + Ok(()) +}