use std::path::Path;
use anyhow::{Context, Result};
use crate::cpu_gemm::{PackedWeight, gemm_packed};
use crate::weights::LazySt;
use crate::whisper::WhisperEncoder;
use crate::whisper_gpu::WhisperEncoderGpu;
#[derive(Debug, Clone)]
pub struct ArkConfig {
pub audio_dim: usize,
pub hidden_size: usize,
pub merge_factor: usize,
pub audio_token_id: u32,
pub max_whisper_length: usize,
}
impl ArkConfig {
pub fn load(dir: &Path) -> Result<Self> {
let v: serde_json::Value =
serde_json::from_slice(&std::fs::read(dir.join("config.json"))?).context("config")?;
let g = |k: &str| v.get(k).and_then(|x| x.as_u64()).unwrap_or(0) as usize;
let audio_dim = v
.get("whisper_config")
.and_then(|w| w.get("d_model"))
.and_then(|x| x.as_u64())
.context("config.whisper_config.d_model")? as usize;
Ok(Self {
audio_dim,
hidden_size: g("hidden_size"),
merge_factor: g("merge_factor").max(1),
audio_token_id: g("audio_token_id") as u32,
max_whisper_length: if g("max_whisper_length") == 0 {
1500
} else {
g("max_whisper_length")
},
})
}
}
struct Linear {
packed: PackedWeight,
bias: Vec<f32>,
out: usize,
inp: usize,
}
impl Linear {
fn load(st: &LazySt, prefix: &str, out: usize, inp: usize) -> Result<Self> {
let w = st.tensor_f32(&format!("{prefix}.weight"))?;
anyhow::ensure!(
w.len() == out * inp,
"{prefix}.weight has {} elements, expected {out}×{inp}",
w.len()
);
Ok(Self {
packed: PackedWeight::new(&w, out, inp),
bias: st.tensor_f32(&format!("{prefix}.bias"))?,
out,
inp,
})
}
fn forward(&self, x: &[f32], rows: usize) -> Vec<f32> {
debug_assert_eq!(x.len(), rows * self.inp);
let mut y = vec![0f32; rows * self.out];
gemm_packed(&mut y, x, &self.packed, rows, Some(&self.bias));
y
}
}
struct LayerNorm {
w: Vec<f32>,
b: Vec<f32>,
eps: f32,
}
impl LayerNorm {
fn load(st: &LazySt, prefix: &str) -> Result<Self> {
Ok(Self {
w: st.tensor_f32(&format!("{prefix}.weight"))?,
b: st.tensor_f32(&format!("{prefix}.bias"))?,
eps: 1e-5,
})
}
fn forward(&self, x: &mut [f32]) {
let d = self.w.len();
for row in x.chunks_mut(d) {
let mean = row.iter().sum::<f32>() / d as f32;
let var = row.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / d as f32;
let inv = 1.0 / (var + self.eps).sqrt();
for (i, v) in row.iter_mut().enumerate() {
*v = (*v - mean) * inv * self.w[i] + self.b[i];
}
}
}
}
fn gelu(x: f32) -> f32 {
0.5 * x * (1.0 + libm::erff(x * std::f32::consts::FRAC_1_SQRT_2))
}
enum EncoderPath {
Cpu(Box<WhisperEncoder>),
Gpu(Box<WhisperEncoderGpu>),
}
pub struct ArkAudioTower {
pub cfg: ArkConfig,
encoder: EncoderPath,
norm: LayerNorm,
proj_in: Linear,
proj_out: Linear,
}
impl ArkAudioTower {
pub fn load(dir: &Path) -> Result<Self> {
Self::build(dir, None)
}
pub fn load_on(ctx: &crate::GpuCtx, dir: &Path) -> Result<Self> {
Self::build(dir, Some(ctx))
}
fn build(dir: &Path, ctx: Option<&crate::GpuCtx>) -> Result<Self> {
let cfg = ArkConfig::load(dir)?;
let encoder =
WhisperEncoder::load_embedded(dir, "audio_encoder.whisper", Some("whisper_config"))?;
let encoder = match ctx {
Some(ctx) => EncoderPath::Gpu(Box::new(WhisperEncoderGpu::new(
ctx,
encoder,
cfg.max_whisper_length,
)?)),
None => EncoderPath::Cpu(Box::new(encoder)),
};
let st = LazySt::open(dir)?;
let merged = cfg.audio_dim * cfg.merge_factor;
Ok(Self {
norm: LayerNorm::load(&st, "audio_encoder.layer_norm")?,
proj_in: Linear::load(&st, "audio_encoder.adapting.0", cfg.hidden_size * 2, merged)?,
proj_out: Linear::load(
&st,
"audio_encoder.adapting.2",
cfg.hidden_size,
cfg.hidden_size * 2,
)?,
encoder,
cfg,
})
}
pub fn audio_token_count(&self, mel_frames: usize) -> usize {
(mel_frames.div_ceil(2) / self.cfg.merge_factor).max(1)
}
pub fn encode(&self, ctx: Option<&crate::GpuCtx>, samples_16k: &[f32]) -> Result<Vec<f32>> {
let enc = match &self.encoder {
EncoderPath::Cpu(e) => e.encode(samples_16k),
EncoderPath::Gpu(g) => {
let ctx =
ctx.context("this tower was built with a GPU encoder; pass its GpuCtx")?;
let mel = g.cpu().logmel(samples_16k);
g.forward(ctx, &mel)?
}
};
let (d, k) = (self.cfg.audio_dim, self.cfg.merge_factor);
let mut h = enc;
self.norm.forward(&mut h);
let frames = h.len() / d;
let groups = (frames / k).max(1);
h.resize(groups * k * d, 0.0);
let mid = self.proj_in.forward(&h, groups);
let act: Vec<f32> = mid.into_iter().map(gelu).collect();
Ok(self.proj_out.forward(&act, groups))
}
}
#[cfg(feature = "cli")]
pub const DEFAULT_INSTRUCTION: &str = "Please transcribe this audio.";
#[cfg(feature = "cli")]
pub struct ArkAsr {
pub tower: ArkAudioTower,
gpu: crate::Lfm2Gpu,
tok: tokenizers::Tokenizer,
dir: std::path::PathBuf,
eos: u32,
}
#[cfg(feature = "cli")]
impl ArkAsr {
pub fn load(ctx: &crate::GpuCtx, dir: &Path) -> Result<Self> {
let tower = ArkAudioTower::load_on(ctx, dir)?;
let w = crate::Weights::load(ctx, dir).context("load ARK decoder (Arch::Qwen2)")?;
anyhow::ensure!(
w.cfg.hidden == tower.cfg.hidden_size,
"decoder hidden {} != adapter output {}",
w.cfg.hidden,
tower.cfg.hidden_size
);
let eos =
serde_json::from_slice::<serde_json::Value>(&std::fs::read(dir.join("config.json"))?)?
.get("eos_token_id")
.and_then(|v| v.as_u64())
.unwrap_or(151645) as u32;
Ok(Self {
gpu: crate::Lfm2Gpu::new(ctx, w),
tok: tokenizers::Tokenizer::from_file(dir.join("tokenizer.json"))
.map_err(|e| anyhow::anyhow!("tokenizer: {e}"))?,
dir: dir.to_path_buf(),
eos,
tower,
})
}
fn ids(&self, text: &str) -> Result<Vec<u32>> {
Ok(self
.tok
.encode(text, false)
.map_err(|e| anyhow::anyhow!("encode: {e}"))?
.get_ids()
.to_vec())
}
pub fn transcribe(
&self,
ctx: &crate::GpuCtx,
samples_16k: &[f32],
max_new: usize,
) -> Result<String> {
self.transcribe_with(ctx, samples_16k, DEFAULT_INSTRUCTION, max_new)
}
pub fn transcribe_with(
&self,
ctx: &crate::GpuCtx,
samples_16k: &[f32],
instruction: &str,
max_new: usize,
) -> Result<String> {
self.gpu.reset(ctx);
let hidden = self.tower.cfg.hidden_size;
let audio = self.tower.encode(Some(ctx), samples_16k)?;
let n_audio = audio.len() / hidden;
let pre = self.ids("<|user|><|begin_of_audio|>")?;
let post = self.ids(&format!("<|end_of_audio|>{instruction}<|assistant|>"))?;
let mut pos = 0usize;
for &t in &pre {
self.gpu.forward_only(ctx, t, pos);
pos += 1;
}
for i in 0..n_audio {
self.gpu
.forward_from_embeds_only(ctx, &audio[i * hidden..(i + 1) * hidden], pos)?;
pos += 1;
}
for &t in &post[..post.len().saturating_sub(1)] {
self.gpu.forward_only(ctx, t, pos);
pos += 1;
}
let mut next = *post.last().context("empty prompt tail")?;
let mut out: Vec<u32> = Vec::new();
for _ in 0..max_new {
let logits = self.gpu.forward(ctx, next, pos)?;
pos += 1;
let (best, _) =
logits
.iter()
.enumerate()
.fold((0usize, f32::NEG_INFINITY), |acc, (i, &v)| {
if v > acc.1 { (i, v) } else { acc }
});
next = best as u32;
if next == self.eos {
break;
}
out.push(next);
}
let _ = &self.dir;
Ok(self
.tok
.decode(&out, true)
.map_err(|e| anyhow::anyhow!("decode: {e}"))?
.trim()
.to_string())
}
}