1use crate::lfm3_vl::{Dense, add_bias, erf, load_proj, load_vec, rms_rows};
12use crate::pipeline::Pipeline;
13use crate::pool::Pool;
14use cortiq_core::CmfModel;
15use serde_json::{Value, json};
16use std::sync::Arc;
17
18pub struct Lfm3AudioConfig {
23 pub n_mels: usize,
24 pub channels: usize,
25 pub hidden: usize,
26 pub window: usize,
27 pub heads: usize,
28 pub kv_heads: usize,
29 pub head_dim: usize,
30 pub attention: Vec<bool>,
32 pub conv_kernel: usize,
33 pub norm_eps: f64,
34 pub rope_theta: f64,
35 pub n_fft: usize,
36 pub hop: usize,
37 pub log_floor: f64,
38 pub norm_offset: f64,
39 pub norm_divisor: f64,
40}
41
42pub fn has_audio(model: &CmfModel) -> bool {
44 model
45 .tensors
46 .iter()
47 .any(|t| t.name == "audio_encoder.downsample.conv_in.weight")
48}
49
50impl Lfm3AudioConfig {
51 pub fn from_model(model: &CmfModel) -> Result<Self, String> {
52 let p = model
53 .header
54 .provenance
55 .as_ref()
56 .and_then(|p| p.get("lfm3_audio"))
57 .ok_or("lfm3_audio: the file has no audio config (provenance.lfm3_audio)")?;
58 let cfg = p.get("config").ok_or("lfm3_audio: provenance has no config")?;
59 Self::from_json(cfg, p.get("features").unwrap_or(&Value::Null))
60 }
61
62 pub fn from_json(cfg: &Value, features: &Value) -> Result<Self, String> {
63 let (enc, lfm, n_mels, channels, hidden) = if let Some(e) = cfg.get("encoder") {
64 let lfm = e.get("lfm").ok_or("lfm3_audio: encoder.lfm missing")?;
65 (e, lfm, e.get("dim_in"), e.get("dim_conv"), e.get("dim_out"))
66 } else {
67 let e = cfg
68 .get("audio_encoder_config")
69 .ok_or("lfm3_audio: no encoder config")?;
70 let lfm = e.get("text_config").ok_or("lfm3_audio: audio_encoder_config.text_config missing")?;
71 (e, lfm, e.get("num_mel_bins"), e.get("conv_channels"), e.get("hidden_size"))
72 };
73 let u = |v: Option<&Value>, what: &str| -> Result<usize, String> {
74 v.and_then(Value::as_u64)
75 .map(|x| x as usize)
76 .ok_or(format!("lfm3_audio: {what} missing"))
77 };
78 let heads = u(lfm.get("num_attention_heads"), "num_attention_heads")?;
79 let hidden = u(hidden, "encoder width")?;
80 let attention: Vec<bool> = lfm
81 .get("layer_types")
82 .and_then(Value::as_array)
83 .ok_or("lfm3_audio: layer_types missing")?
84 .iter()
85 .map(|t| t.as_str() != Some("conv"))
86 .collect();
87 let rope = lfm.get("rope_parameters").unwrap_or(lfm);
88 let pre = |key: &str, legacy: &str, default: f64| -> f64 {
89 features
90 .get(key)
91 .or_else(|| cfg.get("preprocessor").and_then(|p| p.get(legacy)))
92 .and_then(Value::as_f64)
93 .unwrap_or(default)
94 };
95 let norm = cfg.get("preprocessor").and_then(|p| p.get("normalization"));
96 Ok(Self {
97 n_mels: u(n_mels, "mel bins")?,
98 channels: u(channels, "conv channels")?,
99 hidden,
100 window: u(enc.get("sliding_window"), "sliding_window")?,
101 heads,
102 kv_heads: u(lfm.get("num_key_value_heads"), "num_key_value_heads")?,
103 head_dim: hidden / heads.max(1),
104 attention,
105 conv_kernel: u(lfm.get("conv_L_cache"), "conv_L_cache")?,
106 norm_eps: lfm.get("norm_eps").and_then(Value::as_f64).unwrap_or(1e-5),
107 rope_theta: rope.get("rope_theta").and_then(Value::as_f64).unwrap_or(1e6),
108 n_fft: pre("n_fft", "n_fft", 400.0) as usize,
109 hop: pre("hop_length", "hop_length", 160.0) as usize,
110 log_floor: pre("log_floor", "logmel_floor", -12.0),
111 norm_offset: features
112 .get("normalization_offset")
113 .or_else(|| norm.and_then(|n| n.get("offset")))
114 .and_then(Value::as_f64)
115 .unwrap_or(4.0),
116 norm_divisor: features
117 .get("normalization_divisor")
118 .or_else(|| norm.and_then(|n| n.get("divisor")))
119 .and_then(Value::as_f64)
120 .unwrap_or(4.0),
121 })
122 }
123}
124
125fn hz_to_mel(hz: f64) -> f64 {
129 let f_sp = 200.0 / 3.0;
130 let (min_log_hz, logstep) = (1000.0, (6.4f64).ln() / 27.0);
131 if hz < min_log_hz {
132 hz / f_sp
133 } else {
134 min_log_hz / f_sp + (hz / min_log_hz).ln() / logstep
135 }
136}
137
138fn mel_to_hz(m: f64) -> f64 {
139 let f_sp = 200.0 / 3.0;
140 let (min_log_hz, logstep) = (1000.0, (6.4f64).ln() / 27.0);
141 let min_log_mel = min_log_hz / f_sp;
142 if m < min_log_mel {
143 m * f_sp
144 } else {
145 min_log_hz * (logstep * (m - min_log_mel)).exp()
146 }
147}
148
149fn mel_filters(n_mels: usize, n_fft: usize, sr: f64) -> Vec<f64> {
152 let bins = n_fft / 2 + 1;
153 let (lo, hi) = (hz_to_mel(0.0), hz_to_mel(sr / 2.0));
154 let edges: Vec<f64> = (0..n_mels + 2)
155 .map(|i| mel_to_hz(lo + (hi - lo) * i as f64 / (n_mels + 1) as f64))
156 .collect();
157 let mut f = vec![0f64; n_mels * bins];
158 for m in 0..n_mels {
159 let norm = 2.0 / (edges[m + 2] - edges[m]);
160 for k in 0..bins {
161 let hz = (sr / 2.0) * k as f64 / (bins - 1) as f64;
162 let down = (hz - edges[m]) / (edges[m + 1] - edges[m]);
163 let up = (edges[m + 2] - hz) / (edges[m + 2] - edges[m + 1]);
164 f[m * bins + k] = down.min(up).max(0.0) * norm;
165 }
166 }
167 f
168}
169
170pub fn num_frames(n: usize, cfg: &Lfm3AudioConfig) -> usize {
172 let padded = if n < cfg.n_fft {
173 cfg.n_fft
174 } else {
175 n + (cfg.hop - (n - cfg.n_fft) % cfg.hop) % cfg.hop
176 };
177 1 + (padded - cfg.n_fft) / cfg.hop
178}
179
180pub fn log_mel(wave: &[f32], cfg: &Lfm3AudioConfig) -> (Vec<f32>, usize) {
183 use rustfft::{FftPlanner, num_complex::Complex};
184 let (n_fft, hop, n_mels) = (cfg.n_fft, cfg.hop, cfg.n_mels);
185 let frames = num_frames(wave.len(), cfg);
186 let bins = n_fft / 2 + 1;
187 let filters = mel_filters(n_mels, n_fft, 16_000.0);
188 let window: Vec<f64> = (0..n_fft)
190 .map(|i| 0.5 - 0.5 * (2.0 * std::f64::consts::PI * i as f64 / n_fft as f64).cos())
191 .collect();
192 let fft = FftPlanner::<f64>::new().plan_fft_forward(n_fft);
193 let mut buf = vec![Complex::new(0.0, 0.0); n_fft];
194 let mut power = vec![0f64; bins];
195 let mut out = vec![0f32; n_mels * frames];
196 for t in 0..frames {
197 for (i, b) in buf.iter_mut().enumerate() {
198 let s = wave.get(t * hop + i).copied().unwrap_or(0.0);
199 *b = Complex::new(s as f64 * window[i], 0.0);
200 }
201 fft.process(&mut buf);
202 for (p, b) in power.iter_mut().zip(&buf[..bins]) {
203 *p = b.norm_sqr();
204 }
205 for m in 0..n_mels {
206 let e: f64 = filters[m * bins..(m + 1) * bins]
207 .iter()
208 .zip(&power)
209 .map(|(f, p)| f * p)
210 .sum();
211 let l = e.log10().max(cfg.log_floor);
212 out[m * frames + t] = ((l + cfg.norm_offset) / cfg.norm_divisor) as f32;
213 }
214 }
215 (out, frames)
216}
217
218#[allow(clippy::too_many_arguments)]
224fn conv3x3_s2(
225 x: &[f32],
226 h: usize,
227 w: usize,
228 c_in: usize,
229 weight: &[f32],
230 bias: &[f32],
231 c_out: usize,
232 depthwise: bool,
233 pool: Option<&Pool>,
234) -> (Vec<f32>, usize, usize) {
235 let (ho, wo) = (h.div_ceil(2), w.div_ceil(2));
236 let mut y = vec![0f32; ho * wo * c_out];
237 let dst = crate::pool::SendMut::new(y.as_mut_ptr());
238 let f = |lo: usize, hi: usize| {
239 for oy in lo..hi {
240 for ox in 0..wo {
241 let out = unsafe { std::slice::from_raw_parts_mut(dst.at((oy * wo + ox) * c_out), c_out) };
243 out.copy_from_slice(bias);
244 for ky in 0..3 {
245 let iy = (2 * oy + ky) as isize - 1;
246 if iy < 0 || iy >= h as isize {
247 continue;
248 }
249 for kx in 0..3 {
250 let ix = (2 * ox + kx) as isize - 1;
251 if ix < 0 || ix >= w as isize {
252 continue;
253 }
254 let src = &x[(iy as usize * w + ix as usize) * c_in..][..c_in];
255 let tap = ky * 3 + kx;
256 if depthwise {
257 for (c, o) in out.iter_mut().enumerate() {
258 *o += weight[c * 9 + tap] * src[c];
259 }
260 } else {
261 for (c, o) in out.iter_mut().enumerate() {
262 let wk = &weight[c * c_in * 9..];
263 let mut acc = 0f32;
264 for (ci, &v) in src.iter().enumerate() {
265 acc += wk[ci * 9 + tap] * v;
266 }
267 *o += acc;
268 }
269 }
270 }
271 }
272 }
273 }
274 };
275 match pool {
276 Some(p) => p.run_rows(ho, &f),
277 None => f(0, ho),
278 }
279 (y, ho, wo)
280}
281
282fn silu_inplace(x: &mut [f32]) {
283 for v in x.iter_mut() {
284 *v /= 1.0 + (-*v).exp();
285 }
286}
287
288enum Mixer {
289 Conv {
290 in_proj: Dense,
291 taps: Vec<f32>,
293 out_proj: Dense,
294 },
295 Attn {
296 q: Dense,
297 k: Dense,
298 v: Dense,
299 o: Dense,
300 q_norm: Vec<f32>,
301 k_norm: Vec<f32>,
302 },
303}
304
305struct Layer {
306 op_norm: Vec<f32>,
307 ffn_norm: Vec<f32>,
308 mixer: Mixer,
309 w1: Dense,
310 w3: Dense,
311 w2: Dense,
312}
313
314pub struct Lfm3AudioEncoder {
316 pub cfg: Lfm3AudioConfig,
317 conv_in: (Vec<f32>, Vec<f32>),
318 conv_mid: (Vec<f32>, Vec<f32>),
319 lin_mid: (Dense, Vec<f32>),
320 conv_end: (Vec<f32>, Vec<f32>),
321 lin_end: (Dense, Vec<f32>),
322 lin_out: (Dense, Vec<f32>),
323 layers: Vec<Layer>,
324 final_norm: Vec<f32>,
325 ln: (Vec<f32>, Vec<f32>),
326 l1: (Dense, Vec<f32>),
327 l2: (Dense, Vec<f32>),
328 pub text_hidden: usize,
329 pool: Option<Arc<Pool>>,
330}
331
332impl Lfm3AudioEncoder {
333 pub fn from_model(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
334 let cfg = Lfm3AudioConfig::from_model(model)?;
335 let (c, d) = (cfg.channels, cfg.hidden);
336 let ds = |n: &str| format!("audio_encoder.downsample.{n}");
337 let v = |name: String, len: usize| load_vec(model, &name, len);
338 let flat = cfg.n_mels.div_ceil(8) * c;
339 let mut layers = Vec::with_capacity(cfg.attention.len());
340 let inter = model
341 .tensors
342 .iter()
343 .find(|t| t.name == "audio_encoder.lfm.layers.0.feed_forward.w1.weight")
344 .and_then(|t| t.shape.first().copied())
345 .ok_or("lfm3_audio: encoder FFN missing")?;
346 let (qd, kvd) = (cfg.heads * cfg.head_dim, cfg.kv_heads * cfg.head_dim);
347 for (i, &attn) in cfg.attention.iter().enumerate() {
348 let p = format!("audio_encoder.lfm.layers.{i}");
349 let mixer = if attn {
350 Mixer::Attn {
351 q: load_proj(model, &format!("{p}.self_attn.q_proj.weight"), qd, d)?,
352 k: load_proj(model, &format!("{p}.self_attn.k_proj.weight"), kvd, d)?,
353 v: load_proj(model, &format!("{p}.self_attn.v_proj.weight"), kvd, d)?,
354 o: load_proj(model, &format!("{p}.self_attn.out_proj.weight"), d, qd)?,
355 q_norm: v(format!("{p}.self_attn.q_layernorm.weight"), cfg.head_dim)?,
356 k_norm: v(format!("{p}.self_attn.k_layernorm.weight"), cfg.head_dim)?,
357 }
358 } else {
359 Mixer::Conv {
360 in_proj: load_proj(model, &format!("{p}.conv.in_proj.weight"), 3 * d, d)?,
361 taps: v(format!("{p}.conv.conv.weight"), d * cfg.conv_kernel)?,
362 out_proj: load_proj(model, &format!("{p}.conv.out_proj.weight"), d, d)?,
363 }
364 };
365 layers.push(Layer {
366 op_norm: v(format!("{p}.operator_norm.weight"), d)?,
367 ffn_norm: v(format!("{p}.ffn_norm.weight"), d)?,
368 mixer,
369 w1: load_proj(model, &format!("{p}.feed_forward.w1.weight"), inter, d)?,
370 w3: load_proj(model, &format!("{p}.feed_forward.w3.weight"), inter, d)?,
371 w2: load_proj(model, &format!("{p}.feed_forward.w2.weight"), d, inter)?,
372 });
373 }
374 let text_hidden = model
375 .tensors
376 .iter()
377 .find(|t| t.name == "audio_projector.linear_2.weight")
378 .and_then(|t| t.shape.first().copied())
379 .ok_or("lfm3_audio: projector missing")?;
380 let l1_rows = model
381 .tensors
382 .iter()
383 .find(|t| t.name == "audio_projector.linear_1.weight")
384 .and_then(|t| t.shape.first().copied())
385 .ok_or("lfm3_audio: projector missing")?;
386 Ok(Self {
387 conv_in: (v(ds("conv_in.weight"), c * 9)?, v(ds("conv_in.bias"), c)?),
388 conv_mid: (v(ds("conv_mid.weight"), c * 9)?, v(ds("conv_mid.bias"), c)?),
389 lin_mid: (load_proj(model, &ds("lin_mid.weight"), c, c)?, v(ds("lin_mid.bias"), c)?),
390 conv_end: (v(ds("conv_end.weight"), c * 9)?, v(ds("conv_end.bias"), c)?),
391 lin_end: (load_proj(model, &ds("lin_end.weight"), c, c)?, v(ds("lin_end.bias"), c)?),
392 lin_out: (load_proj(model, &ds("lin_out.weight"), d, flat)?, v(ds("lin_out.bias"), d)?),
393 layers,
394 final_norm: v("audio_encoder.lfm.embedding_norm.weight".into(), d)?,
395 ln: (
396 v("audio_projector.layer_norm.weight".into(), d)?,
397 v("audio_projector.layer_norm.bias".into(), d)?,
398 ),
399 l1: (
400 load_proj(model, "audio_projector.linear_1.weight", l1_rows, d)?,
401 v("audio_projector.linear_1.bias".into(), l1_rows)?,
402 ),
403 l2: (
404 load_proj(model, "audio_projector.linear_2.weight", text_hidden, l1_rows)?,
405 v("audio_projector.linear_2.bias".into(), text_hidden)?,
406 ),
407 text_hidden,
408 cfg,
409 pool,
410 })
411 }
412
413 pub fn encode(&self, mel: &[f32], frames: usize) -> Result<Vec<f32>, String> {
416 let c = &self.cfg;
417 let pool = self.pool.as_deref();
418 if mel.len() != c.n_mels * frames || frames == 0 {
419 return Err("lfm3_audio: features do not match the encoder".into());
420 }
421 let (h, w) = (c.n_mels, frames);
423 let ch = c.channels;
424 let (mut x, h1, w1) = conv3x3_s2(mel, h, w, 1, &self.conv_in.0, &self.conv_in.1, ch, false, pool);
425 silu_inplace(&mut x);
426 let (x2, h2, w2) = conv3x3_s2(&x, h1, w1, ch, &self.conv_mid.0, &self.conv_mid.1, ch, true, pool);
427 let mut x = vec![0f32; h2 * w2 * ch];
428 self.lin_mid.0.matmat(&x2, h2 * w2, &mut x, pool);
429 add_bias(&mut x, &self.lin_mid.1);
430 silu_inplace(&mut x);
431 let (x3, h3, w3) = conv3x3_s2(&x, h2, w2, ch, &self.conv_end.0, &self.conv_end.1, ch, true, pool);
432 let mut x = vec![0f32; h3 * w3 * ch];
433 self.lin_end.0.matmat(&x3, h3 * w3, &mut x, pool);
434 add_bias(&mut x, &self.lin_end.1);
435 silu_inplace(&mut x);
436 let n = w3;
438 let flat = ch * h3;
439 let mut f = vec![0f32; n * flat];
440 for y in 0..h3 {
441 for t in 0..n {
442 let src = &x[(y * n + t) * ch..][..ch];
443 for (cc, &val) in src.iter().enumerate() {
444 f[t * flat + cc * h3 + y] = val;
445 }
446 }
447 }
448 let d = c.hidden;
449 let mut x = vec![0f32; n * d];
450 self.lin_out.0.matmat(&f, n, &mut x, pool);
451 add_bias(&mut x, &self.lin_out.1);
452 let mut xn = vec![0f32; n * d];
453 let mut mix = vec![0f32; n * d];
454 let (hd, half) = (c.head_dim, c.head_dim / 2);
455 let inv: Vec<f32> = (0..half)
456 .map(|i| (1.0 / c.rope_theta.powf((2 * i) as f64 / hd as f64)) as f32)
457 .collect();
458 for layer in &self.layers {
459 rms_rows(&x, &layer.op_norm, c.norm_eps, &mut xn, pool);
460 match &layer.mixer {
461 Mixer::Conv { in_proj, taps, out_proj } => {
462 let k = c.conv_kernel;
463 let mut bcx = vec![0f32; n * 3 * d];
464 in_proj.matmat(&xn, n, &mut bcx, pool);
465 let mut y = vec![0f32; n * d];
466 for t in 0..n {
467 for j in 0..d {
468 let mut acc = 0f32;
469 for tap in 0..k {
470 let back = k - 1 - tap;
471 if t >= back {
472 let r = &bcx[(t - back) * 3 * d..];
473 acc += taps[j * k + tap] * (r[j] * r[2 * d + j]);
474 }
475 }
476 y[t * d + j] = bcx[t * 3 * d + d + j] * acc;
477 }
478 }
479 out_proj.matmat(&y, n, &mut mix, pool);
480 }
481 Mixer::Attn { q, k, v, o, q_norm, k_norm } => {
482 let (qd, kvd) = (c.heads * hd, c.kv_heads * hd);
483 let mut qb = vec![0f32; n * qd];
484 let mut kb = vec![0f32; n * kvd];
485 let mut vb = vec![0f32; n * kvd];
486 q.matmat(&xn, n, &mut qb, pool);
487 k.matmat(&xn, n, &mut kb, pool);
488 v.matmat(&xn, n, &mut vb, pool);
489 for (buf, heads, w) in [(&mut qb, c.heads, q_norm), (&mut kb, c.kv_heads, k_norm)] {
490 let mut tmp = vec![0f32; hd];
491 for t in 0..n {
492 for hh in 0..heads {
493 let head = &mut buf[(t * heads + hh) * hd..][..hd];
494 crate::inference::rms_norm_into(
495 head,
496 w,
497 c.norm_eps,
498 cortiq_core::NormStyle::Qwen,
499 &mut tmp,
500 );
501 head.copy_from_slice(&tmp);
502 crate::attention::rope_rotate_scaled(head, t, &inv, 1.0);
503 }
504 }
505 }
506 let group = c.heads / c.kv_heads.max(1);
507 let scale = (hd as f32).powf(-0.5);
508 let mut ao = vec![0f32; n * qd];
509 let mut sc = Vec::with_capacity(c.window);
510 for t in 0..n {
511 let lo = (t + 1).saturating_sub(c.window);
512 for hh in 0..c.heads {
513 let g = hh / group;
514 let qv = &qb[(t * c.heads + hh) * hd..][..hd];
515 sc.clear();
516 for j in lo..=t {
517 let kv = &kb[(j * c.kv_heads + g) * hd..][..hd];
518 sc.push(qv.iter().zip(kv).map(|(a, b)| a * b).sum::<f32>() * scale);
519 }
520 let mx = sc.iter().fold(f32::NEG_INFINITY, |m, &s| m.max(s));
521 let mut sum = 0f32;
522 for s in sc.iter_mut() {
523 *s = (*s - mx).exp();
524 sum += *s;
525 }
526 let out = &mut ao[(t * c.heads + hh) * hd..][..hd];
527 for (j, &p) in (lo..=t).zip(sc.iter()) {
528 let vv = &vb[(j * c.kv_heads + g) * hd..][..hd];
529 for (od, &x) in out.iter_mut().zip(vv) {
530 *od += p / sum * x;
531 }
532 }
533 }
534 }
535 o.matmat(&ao, n, &mut mix, pool);
536 }
537 }
538 for (a, &b) in x.iter_mut().zip(&mix) {
539 *a += b;
540 }
541 rms_rows(&x, &layer.ffn_norm, c.norm_eps, &mut xn, pool);
542 let inter = layer.w1.rows;
543 let mut g = vec![0f32; n * inter];
544 let mut u = vec![0f32; n * inter];
545 layer.w1.matmat(&xn, n, &mut g, pool);
546 layer.w3.matmat(&xn, n, &mut u, pool);
547 for (a, &b) in g.iter_mut().zip(&u) {
548 *a = *a / (1.0 + (-*a).exp()) * b;
549 }
550 layer.w2.matmat(&g, n, &mut mix, pool);
551 for (a, &b) in x.iter_mut().zip(&mix) {
552 *a += b;
553 }
554 }
555 rms_rows(&x, &self.final_norm, c.norm_eps, &mut xn, pool);
556 Ok(xn)
557 }
558
559 pub fn project(&self, enc: &[f32]) -> Vec<f32> {
561 let pool = self.pool.as_deref();
562 let d = self.cfg.hidden;
563 let n = enc.len() / d;
564 let mut x = enc.to_vec();
565 for row in x.chunks_exact_mut(d) {
566 let mean = row.iter().map(|&v| v as f64).sum::<f64>() / d as f64;
567 let var = row.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / d as f64;
568 let inv = 1.0 / (var + 1e-5).sqrt();
569 for (j, v) in row.iter_mut().enumerate() {
570 *v = ((*v as f64 - mean) * inv) as f32 * self.ln.0[j] + self.ln.1[j];
571 }
572 }
573 let mut h1 = vec![0f32; n * self.l1.0.rows];
574 self.l1.0.matmat(&x, n, &mut h1, pool);
575 add_bias(&mut h1, &self.l1.1);
576 for v in h1.iter_mut() {
577 *v = (0.5 * *v as f64 * (1.0 + erf(*v as f64 / std::f64::consts::SQRT_2))) as f32;
578 }
579 let mut out = vec![0f32; n * self.text_hidden];
580 self.l2.0.matmat(&h1, n, &mut out, pool);
581 add_bias(&mut out, &self.l2.1);
582 out
583 }
584
585 pub fn rows(&self, wave: &[f32]) -> Result<Vec<f32>, String> {
587 let (mel, frames) = log_mel(wave, &self.cfg);
588 let enc = self.encode(&mel, frames)?;
589 let rows = self.project(&enc);
590 if !rows.iter().all(|v| v.is_finite()) {
591 return Err("lfm3_audio: the encoder produced non-finite rows".into());
592 }
593 Ok(rows)
594 }
595}
596
597pub(crate) fn normalize(messages: &[Value]) -> (Vec<Value>, Vec<Value>) {
602 let mut messages = messages.to_vec();
603 let mut sources = Vec::new();
604 for message in &mut messages {
605 let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
606 continue;
607 };
608 for part in parts {
609 let typ = part.get("type").and_then(Value::as_str).unwrap_or("");
610 if matches!(typ, "audio" | "input_audio" | "audio_url")
611 || part.get("input_audio").is_some()
612 || part.get("audio").is_some()
613 {
614 let src = part
615 .get("input_audio")
616 .or_else(|| part.get("audio_url"))
617 .or_else(|| part.get("audio"))
618 .cloned()
619 .unwrap_or_else(|| part.clone());
620 let src = match (&src, src.get("url")) {
621 (Value::Object(_), Some(u)) if src.get("data").is_none() => u.clone(),
622 _ => src,
623 };
624 sources.push(src);
625 *part = json!({"type": "audio"});
626 }
627 }
628 }
629 (messages, sources)
630}
631
632pub fn has_audio_parts(messages: &[Value]) -> bool {
634 !normalize(messages).1.is_empty()
635}
636
637pub fn prepare(
642 model: &Arc<CmfModel>,
643 pipeline: &Pipeline,
644 messages: &[Value],
645 tools: Option<&[Value]>,
646 thinking: Option<bool>,
647) -> Result<crate::lfm3_vl::PreparedVlPrompt, String> {
648 let (messages, sources) = normalize(messages);
649 let tok = &pipeline.tokenizer;
650 let mut text = tok
651 .render_chat_json(&messages, tools, thinking)
652 .ok_or("LFM3-Audio: the chat template failed to render")?;
653 if thinking == Some(false) && text.ends_with("<|im_start|>assistant\n") {
656 text.push_str("<think></think>");
657 }
658 if sources.is_empty() {
659 return Ok(crate::lfm3_vl::PreparedVlPrompt { token_ids: tok.with_bos(tok.encode(&text)), rows: None });
660 }
661 let enc = Lfm3AudioEncoder::from_model(model, crate::pool::Pool::from_env())?;
662 if enc.text_hidden != pipeline.hidden_size {
663 return Err("LFM3-Audio: projector width differs from the text model".into());
664 }
665 let audio_id = tok
666 .token_to_id("<|audio|>")
667 .ok_or("LFM3-Audio: the tokenizer has no <|audio|> token")?;
668 let dump = std::env::var_os("CMF_LFM3_AUDIO_DUMP").map(std::path::PathBuf::from);
669 let mut sources = sources.into_iter();
670 let mut expanded = String::with_capacity(text.len());
671 let mut media_rows: Vec<f32> = Vec::new();
672 let mut rest = text.as_str();
673 let mut k = 0usize;
674 while let Some(at) = rest.find("<|audio|>") {
675 expanded.push_str(&rest[..at]);
676 rest = &rest[at + "<|audio|>".len()..];
677 let src = sources
678 .next()
679 .ok_or("LFM3-Audio: more <|audio|> placeholders than clips")?;
680 let wave = crate::egemma2_audio::load_audio_source(&src)?;
681 let t0 = std::time::Instant::now();
682 let (mel, frames) = log_mel(&wave, &enc.cfg);
683 let er = enc.encode(&mel, frames)?;
684 let rows = enc.project(&er);
685 if let Some(dir) = &dump {
686 let write = |name: String, v: &[f32]| {
687 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
688 std::fs::write(dir.join(name), bytes).map_err(|e| format!("CMF_LFM3_AUDIO_DUMP: {e}"))
689 };
690 write(format!("mel_{k}.f32"), &mel)?;
691 write(format!("enc_{k}.f32"), &er)?;
692 write(format!("rows_{k}.f32"), &rows)?;
693 eprintln!(
694 "lfm3-audio: clip {k}: {} samples, {frames} frames, {} tokens, encoder {:.2} s",
695 wave.len(),
696 rows.len() / enc.text_hidden,
697 t0.elapsed().as_secs_f64()
698 );
699 }
700 for _ in 0..rows.len() / enc.text_hidden {
701 expanded.push_str("<|audio|>");
702 }
703 media_rows.extend(rows);
704 k += 1;
705 }
706 expanded.push_str(rest);
707 if sources.next().is_some() {
708 return Err("LFM3-Audio: the template rendered fewer placeholders than clips".into());
709 }
710 let ids = tok.with_bos(tok.encode(&expanded));
711 let h = pipeline.hidden_size;
712 let mut rows = Vec::with_capacity(ids.len() * h);
713 let mut at = 0usize;
714 for &id in &ids {
715 if id == audio_id {
716 let row = media_rows
717 .get(at..at + h)
718 .ok_or("LFM3-Audio: placeholder and clip row counts differ")?;
719 rows.extend_from_slice(row);
720 at += h;
721 } else {
722 rows.extend(pipeline.embed_id(id));
723 }
724 }
725 if at != media_rows.len() {
726 return Err("LFM3-Audio: unused clip rows".into());
727 }
728 Ok(crate::lfm3_vl::PreparedVlPrompt { token_ids: ids, rows: Some(rows) })
729}
730
731pub fn text_only_suppress(tok: &crate::tokenizer::Tokenizer) -> Vec<u32> {
734 ["<|audio_start|>", "<|mixed_start|>"]
735 .iter()
736 .filter_map(|t| tok.token_to_id(t))
737 .collect()
738}
739
740pub fn prepare_for_pipeline(
742 pipeline: &Pipeline,
743 messages: &[Value],
744 tools: Option<&[Value]>,
745 thinking: Option<bool>,
746) -> Result<crate::lfm3_vl::PreparedVlPrompt, String> {
747 let model = pipeline
748 .model
749 .as_ref()
750 .ok_or("LFM3-Audio clips require a file-backed pipeline")?;
751 prepare(model, pipeline, messages, tools, thinking)
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 fn cfg() -> Lfm3AudioConfig {
759 Lfm3AudioConfig {
760 n_mels: 128, channels: 256, hidden: 768, window: 32, heads: 8, kv_heads: 4,
761 head_dim: 96, attention: vec![], conv_kernel: 3, norm_eps: 1e-5, rope_theta: 1e6,
762 n_fft: 400, hop: 160, log_floor: -12.0, norm_offset: 4.0, norm_divisor: 4.0,
763 }
764 }
765
766 #[test]
769 fn frames_match_the_processor() {
770 let c = cfg();
771 assert_eq!(num_frames(100, &c), 1);
772 assert_eq!(num_frames(400, &c), 1);
773 assert_eq!(num_frames(401, &c), 2);
774 assert_eq!(num_frames(560, &c), 2);
775 assert_eq!(num_frames(70_080, &c), 437);
777 }
778
779 #[test]
782 fn slaney_filters_are_triangles() {
783 let f = mel_filters(128, 400, 16_000.0);
784 assert!(f.iter().all(|&v| v >= 0.0));
785 assert!(f[..201].iter().any(|&v| v > 0.0));
786 }
787}