Skip to main content

cortiq_engine/
lfm3_speech.rs

1//! LFM3-Audio / LFM3-Omni speech output. After `<|audio_start|>` every step
2//! of the language model yields one speech frame: the depth decoder (a
3//! three-layer bidirectional LFM3 over the frame's 12 codebook slots) reads
4//! the step's hidden state and reveals the codebooks in three MaskGIT
5//! passes of four; the frame's codebook embeddings, summed, are the next
6//! step's input; a frame holding the end-of-audio code ends the speech.
7//! LiquidCodec's decoder turns the frames into 24 kHz audio: FSQ codes →
8//! 48 latents → 256 → 512, six steps a frame through an eight-layer LFM3
9//! (short convolutions, attention causal within 30 steps), log-magnitude
10//! and phase of a 1280-point spectrum, inverse STFT with hop 320.
11//! Everything here runs in f32 on the host from the f16 tensors the
12//! converter keeps (`depth_decoder.*`, `codebook_embedding`, `codec.*`).
13
14use crate::lfm3_vl::{Dense, add_bias, load_proj, load_vec};
15use crate::pipeline::Pipeline;
16use crate::pool::Pool;
17use crate::sampler::SplitMix64;
18use cortiq_core::CmfModel;
19use std::sync::Arc;
20
21/// Codebooks of a speech frame and codes of a codebook (the last one is
22/// the end-of-audio code).
23pub const N_CODEBOOKS: usize = 12;
24pub const CODEBOOK_SIZE: usize = 2017;
25pub const AUDIO_EOS: u32 = 2016;
26/// Codebooks revealed by each MaskGIT pass.
27const MASKGIT: [usize; 3] = [4, 4, 4];
28/// Samples a frame decodes to at 24 kHz.
29pub const FRAME_SAMPLES: usize = 1920;
30
31fn tensor_rows(model: &CmfModel, name: &str) -> Option<usize> {
32    model.tensors.iter().find(|t| t.name == name).and_then(|t| t.shape.first().copied())
33}
34
35fn has_tensor(model: &CmfModel, name: &str) -> bool {
36    model.tensors.iter().any(|t| t.name == name)
37}
38
39// ───────────────────────────── an LFM stack ─────────────────────────────
40
41enum Mixer {
42    Conv { in_proj: Dense, taps: Vec<f32>, out_proj: Dense },
43    Attn { q: Dense, k: Dense, v: Dense, o: Dense, q_norm: Vec<f32>, k_norm: Vec<f32>, gate: Option<Dense> },
44}
45
46struct Layer {
47    op_norm: Vec<f32>,
48    post_op: Option<Vec<f32>>,
49    ffn_norm: Vec<f32>,
50    post_ffn: Option<Vec<f32>>,
51    mixer: Mixer,
52    w1: Dense,
53    w3: Dense,
54    w2: Dense,
55}
56
57/// An LFM3 layer stack in Liquid's names (`operator_norm`, `conv.*`,
58/// `self_attn.*`, `feed_forward.w1..3`, the optional sandwich norms
59/// `post_operator_norm` / `post_ffn_norm`, the optional per-head sigmoid
60/// gate `self_attn.gate_proj`), final `embedding_norm`.
61struct Stack {
62    layers: Vec<Layer>,
63    final_norm: Vec<f32>,
64    d: usize,
65    heads: usize,
66    kv: usize,
67    hd: usize,
68    conv_k: usize,
69    eps: f64,
70    inv: Vec<f32>,
71    /// Some(w): causal within w positions; None: bidirectional.
72    window: Option<usize>,
73}
74
75fn rms(x: &[f32], w: &[f32], eps: f64, out: &mut [f32]) {
76    let d = w.len();
77    for (src, dst) in x.chunks_exact(d).zip(out.chunks_exact_mut(d)) {
78        let ss = src.iter().map(|&v| v as f64 * v as f64).sum::<f64>() / d as f64;
79        let inv = 1.0 / (ss + eps).sqrt();
80        for ((o, &v), &g) in dst.iter_mut().zip(src).zip(w) {
81            *o = (v as f64 * inv) as f32 * g;
82        }
83    }
84}
85
86impl Stack {
87    #[allow(clippy::too_many_arguments)]
88    fn load(
89        model: &Arc<CmfModel>,
90        prefix: &str,
91        d: usize,
92        heads: usize,
93        kv: usize,
94        eps: f64,
95        theta: f64,
96        window: Option<usize>,
97    ) -> Result<Self, String> {
98        let hd = d / heads;
99        let mut layers = Vec::new();
100        let mut conv_k = 1;
101        for i in 0.. {
102            let p = format!("{prefix}.layers.{i}");
103            let conv = has_tensor(model, &format!("{p}.conv.in_proj.weight"));
104            let attn = has_tensor(model, &format!("{p}.self_attn.q_proj.weight"));
105            if !conv && !attn {
106                break;
107            }
108            let v = |n: &str, len: usize| load_vec(model, &format!("{p}.{n}"), len);
109            let opt = |n: &str| {
110                has_tensor(model, &format!("{p}.{n}")).then(|| v(n, d)).transpose()
111            };
112            let mixer = if conv {
113                let taps_len = model
114                    .tensors
115                    .iter()
116                    .find(|t| t.name == format!("{p}.conv.conv.weight"))
117                    .map(|t| t.shape.iter().product::<usize>())
118                    .ok_or("lfm stack: conv taps missing")?;
119                conv_k = taps_len / d;
120                Mixer::Conv {
121                    in_proj: load_proj(model, &format!("{p}.conv.in_proj.weight"), 3 * d, d)?,
122                    taps: v("conv.conv.weight", taps_len)?,
123                    out_proj: load_proj(model, &format!("{p}.conv.out_proj.weight"), d, d)?,
124                }
125            } else {
126                let gate_name = format!("{p}.self_attn.gate_proj.weight");
127                Mixer::Attn {
128                    q: load_proj(model, &format!("{p}.self_attn.q_proj.weight"), heads * hd, d)?,
129                    k: load_proj(model, &format!("{p}.self_attn.k_proj.weight"), kv * hd, d)?,
130                    v: load_proj(model, &format!("{p}.self_attn.v_proj.weight"), kv * hd, d)?,
131                    o: load_proj(model, &format!("{p}.self_attn.o_proj.weight"), d, heads * hd)?,
132                    q_norm: v("self_attn.q_norm.weight", hd)?,
133                    k_norm: v("self_attn.k_norm.weight", hd)?,
134                    gate: has_tensor(model, &gate_name)
135                        .then(|| load_proj(model, &gate_name, heads, d))
136                        .transpose()?,
137                }
138            };
139            let inter = tensor_rows(model, &format!("{p}.feed_forward.w1.weight")).ok_or("lfm stack: FFN missing")?;
140            layers.push(Layer {
141                op_norm: v("operator_norm.weight", d)?,
142                post_op: opt("post_operator_norm.weight")?,
143                ffn_norm: v("ffn_norm.weight", d)?,
144                post_ffn: opt("post_ffn_norm.weight")?,
145                mixer,
146                w1: load_proj(model, &format!("{p}.feed_forward.w1.weight"), inter, d)?,
147                w3: load_proj(model, &format!("{p}.feed_forward.w3.weight"), inter, d)?,
148                w2: load_proj(model, &format!("{p}.feed_forward.w2.weight"), d, inter)?,
149            });
150        }
151        if layers.is_empty() {
152            return Err(format!("lfm stack: no layers under {prefix}"));
153        }
154        let inv = (0..hd / 2)
155            .map(|i| (1.0 / theta.powf((2 * i) as f64 / hd as f64)) as f32)
156            .collect();
157        Ok(Self {
158            layers,
159            final_norm: load_vec(model, &format!("{prefix}.embedding_norm.weight"), d)?,
160            d,
161            heads,
162            kv,
163            hd,
164            conv_k,
165            eps,
166            inv,
167            window,
168        })
169    }
170
171    /// `x` `[n, d]` through every layer and the final norm, in place.
172    fn forward(&self, x: &mut [f32], pool: Option<&Pool>) {
173        let (d, hd) = (self.d, self.hd);
174        let n = x.len() / d;
175        let mut xn = vec![0f32; n * d];
176        let mut mix = vec![0f32; n * d];
177        let mut tmp = vec![0f32; n * d];
178        for l in &self.layers {
179            rms(x, &l.op_norm, self.eps, &mut xn);
180            match &l.mixer {
181                Mixer::Conv { in_proj, taps, out_proj } => {
182                    let k = self.conv_k;
183                    let mut bcx = vec![0f32; n * 3 * d];
184                    in_proj.matmat(&xn, n, &mut bcx, pool);
185                    let mut y = vec![0f32; n * d];
186                    for t in 0..n {
187                        for j in 0..d {
188                            let mut acc = 0f32;
189                            for tap in 0..k {
190                                let back = k - 1 - tap;
191                                if t >= back {
192                                    let r = &bcx[(t - back) * 3 * d..];
193                                    acc += taps[j * k + tap] * (r[j] * r[2 * d + j]);
194                                }
195                            }
196                            y[t * d + j] = bcx[t * 3 * d + d + j] * acc;
197                        }
198                    }
199                    out_proj.matmat(&y, n, &mut mix, pool);
200                }
201                Mixer::Attn { q, k, v, o, q_norm, k_norm, gate } => {
202                    let (qd, kvd) = (self.heads * hd, self.kv * hd);
203                    let mut qb = vec![0f32; n * qd];
204                    let mut kb = vec![0f32; n * kvd];
205                    let mut vb = vec![0f32; n * kvd];
206                    q.matmat(&xn, n, &mut qb, pool);
207                    k.matmat(&xn, n, &mut kb, pool);
208                    v.matmat(&xn, n, &mut vb, pool);
209                    let mut head = vec![0f32; hd];
210                    for (buf, heads, w) in [(&mut qb, self.heads, q_norm), (&mut kb, self.kv, k_norm)] {
211                        for t in 0..n {
212                            for hh in 0..heads {
213                                let hv = &mut buf[(t * heads + hh) * hd..][..hd];
214                                rms(hv, w, self.eps, &mut head);
215                                hv.copy_from_slice(&head);
216                                crate::attention::rope_rotate_scaled(hv, t, &self.inv, 1.0);
217                            }
218                        }
219                    }
220                    let group = self.heads / self.kv.max(1);
221                    let scale = (hd as f32).powf(-0.5);
222                    let mut ao = vec![0f32; n * qd];
223                    let mut sc = Vec::new();
224                    for t in 0..n {
225                        let (lo, hi) = match self.window {
226                            Some(w) => ((t + 1).saturating_sub(w), t + 1),
227                            None => (0, n),
228                        };
229                        for hh in 0..self.heads {
230                            let g = hh / group;
231                            let qv = &qb[(t * self.heads + hh) * hd..][..hd];
232                            sc.clear();
233                            for j in lo..hi {
234                                let kv = &kb[(j * self.kv + g) * hd..][..hd];
235                                sc.push(qv.iter().zip(kv).map(|(a, b)| a * b).sum::<f32>() * scale);
236                            }
237                            let mx = sc.iter().fold(f32::NEG_INFINITY, |m, &s| m.max(s));
238                            let mut sum = 0f32;
239                            for s in sc.iter_mut() {
240                                *s = (*s - mx).exp();
241                                sum += *s;
242                            }
243                            let out = &mut ao[(t * self.heads + hh) * hd..][..hd];
244                            for (j, &p) in (lo..hi).zip(sc.iter()) {
245                                let vv = &vb[(j * self.kv + g) * hd..][..hd];
246                                for (od, &x) in out.iter_mut().zip(vv) {
247                                    *od += p / sum * x;
248                                }
249                            }
250                        }
251                    }
252                    if let Some(g) = gate {
253                        let mut raw = vec![0f32; n * self.heads];
254                        g.matmat(&xn, n, &mut raw, pool);
255                        for t in 0..n {
256                            for hh in 0..self.heads {
257                                let s = 1.0 / (1.0 + (-raw[t * self.heads + hh]).exp());
258                                for a in &mut ao[(t * self.heads + hh) * hd..][..hd] {
259                                    *a *= s;
260                                }
261                            }
262                        }
263                    }
264                    o.matmat(&ao, n, &mut mix, pool);
265                }
266            }
267            if let Some(w) = &l.post_op {
268                rms(&mix, w, self.eps, &mut tmp);
269                std::mem::swap(&mut mix, &mut tmp);
270            }
271            for (a, &b) in x.iter_mut().zip(&mix) {
272                *a += b;
273            }
274            rms(x, &l.ffn_norm, self.eps, &mut xn);
275            let inter = l.w1.rows;
276            let mut g = vec![0f32; n * inter];
277            let mut u = vec![0f32; n * inter];
278            l.w1.matmat(&xn, n, &mut g, pool);
279            l.w3.matmat(&xn, n, &mut u, pool);
280            for (a, &b) in g.iter_mut().zip(&u) {
281                *a = *a / (1.0 + (-*a).exp()) * b;
282            }
283            l.w2.matmat(&g, n, &mut mix, pool);
284            if let Some(w) = &l.post_ffn {
285                rms(&mix, w, self.eps, &mut tmp);
286                std::mem::swap(&mut mix, &mut tmp);
287            }
288            for (a, &b) in x.iter_mut().zip(&mix) {
289                *a += b;
290            }
291        }
292        rms(x, &self.final_norm, self.eps, &mut xn);
293        x.copy_from_slice(&xn);
294    }
295}
296
297// ───────────────────────────── depth decoder ─────────────────────────────
298
299/// Audio sampling: Liquid's recommended `audio_temperature` 1.0,
300/// `audio_top_k` 128, repetition penalty 1.1 over the last 4096 frames.
301#[derive(Clone, Debug)]
302pub struct AudioSampling {
303    pub temperature: f32,
304    pub top_k: usize,
305    pub repetition_penalty: f32,
306    pub repetition_window: usize,
307    /// Reveal the codebooks in order 0..11 instead of a random order (the
308    /// reference harness's fixed order).
309    pub fixed_order: bool,
310}
311
312impl Default for AudioSampling {
313    fn default() -> Self {
314        Self { temperature: 1.0, top_k: 128, repetition_penalty: 1.1, repetition_window: 4096, fixed_order: false }
315    }
316}
317
318impl AudioSampling {
319    /// The release's own `audio_*` settings (its generation config, kept by
320    /// the converter in `provenance.lfm3_audio.generation`): LFM3-Omni
321    /// samples at 0.4 with a 1.2 penalty over 64 frames and no top-k.
322    pub fn from_model(model: &CmfModel) -> Self {
323        let mut s = Self::default();
324        let g = model
325            .header
326            .provenance
327            .as_ref()
328            .and_then(|p| p.get("lfm3_audio"))
329            .and_then(|a| a.get("generation"));
330        if let Some(g) = g {
331            if let Some(t) = g.get("audio_temperature").and_then(|v| v.as_f64()) {
332                s.temperature = t as f32;
333            }
334            match g.get("audio_top_k") {
335                Some(v) if v.is_null() => s.top_k = 0,
336                Some(v) => s.top_k = v.as_u64().map_or(s.top_k, |k| k as usize),
337                None => {}
338            }
339            if let Some(r) = g.get("audio_repetition_penalty").and_then(|v| v.as_f64()) {
340                s.repetition_penalty = r as f32;
341            }
342            if let Some(w) = g.get("audio_repetition_window").and_then(|v| v.as_u64()) {
343                s.repetition_window = w as usize;
344            }
345        }
346        s
347    }
348}
349
350/// The depth decoder and the language model's speech-frame embedding.
351pub struct DepthDecoder {
352    linear: (Dense, Vec<f32>),
353    mask: Vec<f32>,
354    /// `[12][2017][d]` code embeddings (also the tied output heads).
355    emb: Vec<f32>,
356    emb_norm: Vec<f32>,
357    stack: Stack,
358    d: usize,
359    /// `[12·2017][text_hidden]`, the frame embedding of the language model.
360    codebook: Vec<f32>,
361    text_hidden: usize,
362    pool: Option<Arc<Pool>>,
363}
364
365impl DepthDecoder {
366    pub fn from_model(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
367        let text_hidden = model
368            .tensors
369            .iter()
370            .find(|t| t.name == "codebook_embedding.weight")
371            .and_then(|t| t.shape.get(1).copied())
372            .ok_or("lfm3_speech: codebook_embedding missing")?;
373        let d = tensor_rows(model, "depth_decoder.mask_embedding").ok_or("lfm3_speech: mask_embedding missing")?;
374        let mut emb = Vec::with_capacity(N_CODEBOOKS * CODEBOOK_SIZE * d);
375        let mut emb_norm = Vec::with_capacity(N_CODEBOOKS * d);
376        for i in 0..N_CODEBOOKS {
377            let p = format!("depth_decoder.depth_embeddings.{i}");
378            emb.extend(load_vec(model, &format!("{p}.embedding.weight"), CODEBOOK_SIZE * d)?);
379            emb_norm.extend(load_vec(model, &format!("{p}.embedding_norm.weight"), d)?);
380        }
381        // Geometry of the depth model: 32-wide heads (its config).
382        let q_rows = tensor_rows(model, "depth_decoder.model.layers.0.self_attn.q_proj.weight").unwrap_or(d);
383        let k_rows = tensor_rows(model, "depth_decoder.model.layers.0.self_attn.k_proj.weight").unwrap_or(d);
384        let hd = model
385            .tensors
386            .iter()
387            .find(|t| t.name == "depth_decoder.model.layers.0.self_attn.q_norm.weight")
388            .and_then(|t| t.shape.first().copied())
389            .ok_or("lfm3_speech: depth q_norm missing")?;
390        let stack = Stack::load(model, "depth_decoder.model", d, q_rows / hd, k_rows / hd, 1e-5, 1e6, None)?;
391        Ok(Self {
392            linear: (
393                load_proj(model, "depth_decoder.depth_linear.weight", N_CODEBOOKS * d, text_hidden)?,
394                load_vec(model, "depth_decoder.depth_linear.bias", N_CODEBOOKS * d)?,
395            ),
396            mask: load_vec(model, "depth_decoder.mask_embedding", d)?,
397            emb,
398            emb_norm,
399            stack,
400            d,
401            codebook: load_vec(model, "codebook_embedding.weight", N_CODEBOOKS * CODEBOOK_SIZE * text_hidden)?,
402            text_hidden,
403            pool,
404        })
405    }
406
407    /// The frame's slots before any code: the language model's
408    /// final-normed hidden state through `depth_linear`, `[12][d]`.
409    fn slots(&self, h: &[f32]) -> Vec<f32> {
410        let mut x = vec![0f32; N_CODEBOOKS * self.d];
411        self.linear.0.matmat(h, 1, &mut x, self.pool.as_deref());
412        add_bias(&mut x, &self.linear.1);
413        x
414    }
415
416    /// Logits `[2017]` of the codebooks `want` (in that order) given the
417    /// frame's slots, its known codes and which are still hidden.
418    fn logits(
419        &self,
420        slots: &[f32],
421        codes: &[u32; N_CODEBOOKS],
422        masked: &[bool; N_CODEBOOKS],
423        want: &[usize],
424    ) -> Vec<f32> {
425        let pool = self.pool.as_deref();
426        let d = self.d;
427        let mut x = slots.to_vec();
428        for i in 0..N_CODEBOOKS {
429            let add = if masked[i] {
430                &self.mask[..]
431            } else {
432                &self.emb[(i * CODEBOOK_SIZE + codes[i] as usize) * d..][..d]
433            };
434            for (a, &b) in x[i * d..(i + 1) * d].iter_mut().zip(add) {
435                *a += b;
436            }
437        }
438        self.stack.forward(&mut x, pool);
439        let mut out = vec![0f32; want.len() * CODEBOOK_SIZE];
440        let mut normed = vec![0f32; d];
441        for (r, &i) in want.iter().enumerate() {
442            rms(&x[i * d..(i + 1) * d], &self.emb_norm[i * d..(i + 1) * d], 1e-5, &mut normed);
443            let table = &self.emb[i * CODEBOOK_SIZE * d..(i + 1) * CODEBOOK_SIZE * d];
444            let row = &mut out[r * CODEBOOK_SIZE..(r + 1) * CODEBOOK_SIZE];
445            crate::fcd_ops::gemm_nt_host(&normed, table, row, 1, d, CODEBOOK_SIZE, pool);
446        }
447        out
448    }
449
450    /// One speech frame: the codebooks revealed four at a time in a random
451    /// (or fixed) order, each sampled from its logits under the repetition
452    /// penalty of its own history.
453    pub fn sample_frame(
454        &self,
455        h: &[f32],
456        s: &AudioSampling,
457        history: &[[u32; N_CODEBOOKS]],
458        rng: &mut SplitMix64,
459    ) -> [u32; N_CODEBOOKS] {
460        let mut order: Vec<usize> = (0..N_CODEBOOKS).collect();
461        if !s.fixed_order {
462            for i in (1..N_CODEBOOKS).rev() {
463                let j = (rng.next_u64() % (i as u64 + 1)) as usize;
464                order.swap(i, j);
465            }
466        }
467        let mut codes = [0u32; N_CODEBOOKS];
468        let mut masked = [true; N_CODEBOOKS];
469        let hist = &history[history.len().saturating_sub(s.repetition_window)..];
470        let slots = self.slots(h);
471        let mut at = 0;
472        for step in MASKGIT {
473            let pick = &order[at..at + step];
474            let logits = self.logits(&slots, &codes, &masked, pick);
475            for (r, &cb) in pick.iter().enumerate() {
476                let mut row = logits[r * CODEBOOK_SIZE..(r + 1) * CODEBOOK_SIZE].to_vec();
477                if s.repetition_penalty != 1.0 {
478                    let mut seen = vec![false; CODEBOOK_SIZE];
479                    for f in hist {
480                        seen[f[cb] as usize] = true;
481                    }
482                    for (v, &was) in row.iter_mut().zip(&seen) {
483                        if was {
484                            *v = if *v < 0.0 { *v * s.repetition_penalty } else { *v / s.repetition_penalty };
485                        }
486                    }
487                }
488                codes[cb] = sample(&row, s.temperature, s.top_k, rng);
489            }
490            for &cb in &order[at..at + step] {
491                masked[cb] = false;
492            }
493            at += step;
494        }
495        codes
496    }
497
498    /// The language model's input row for a frame: its codebook
499    /// embeddings, summed.
500    pub fn frame_row(&self, codes: &[u32; N_CODEBOOKS]) -> Vec<f32> {
501        let th = self.text_hidden;
502        let mut row = vec![0f32; th];
503        for (i, &c) in codes.iter().enumerate() {
504            let e = &self.codebook[(i * CODEBOOK_SIZE + c as usize) * th..][..th];
505            for (a, &b) in row.iter_mut().zip(e) {
506                *a += b;
507            }
508        }
509        row
510    }
511}
512
513/// Greedy at temperature 0, else temperature then top-k then a draw.
514fn sample(logits: &[f32], temperature: f32, top_k: usize, rng: &mut SplitMix64) -> u32 {
515    if temperature <= 0.0 {
516        return logits
517            .iter()
518            .enumerate()
519            .fold((0, f32::NEG_INFINITY), |b, (i, &v)| if v > b.1 { (i, v) } else { b })
520            .0 as u32;
521    }
522    let mut idx: Vec<usize> = (0..logits.len()).collect();
523    if top_k > 0 && top_k < idx.len() {
524        idx.select_nth_unstable_by(top_k - 1, |&a, &b| logits[b].total_cmp(&logits[a]));
525        idx.truncate(top_k);
526    }
527    let mx = idx.iter().map(|&i| logits[i] / temperature).fold(f32::NEG_INFINITY, f32::max);
528    let w: Vec<f64> = idx.iter().map(|&i| ((logits[i] / temperature - mx) as f64).exp()).collect();
529    let total: f64 = w.iter().sum();
530    let mut r = rng.next_f32() as f64 * total;
531    for (&i, &wi) in idx.iter().zip(&w) {
532        r -= wi;
533        if r <= 0.0 {
534            return i as u32;
535        }
536    }
537    *idx.last().unwrap() as u32
538}
539
540// ───────────────────────────── codec ─────────────────────────────
541
542/// LiquidCodec's decoder: speech frames to 24 kHz audio.
543pub struct Codec {
544    project_out: (Dense, Vec<f32>),
545    from_q: (Dense, Vec<f32>),
546    stack: Stack,
547    lin: (Dense, Vec<f32>),
548    window: Vec<f32>,
549    pool: Option<Arc<Pool>>,
550}
551
552const FSQ_LEVELS: [u32; 4] = [8, 7, 6, 6];
553const FRAME_STACK: usize = 6;
554const N_FFT: usize = 1280;
555const HOP: usize = 320;
556
557impl Codec {
558    pub fn from_model(model: &Arc<CmfModel>, pool: Option<Arc<Pool>>) -> Result<Self, String> {
559        let qd = tensor_rows(model, "codec.quantizer.project_out.weight").ok_or("lfm3_speech: the file has no codec")?;
560        let ed = tensor_rows(model, "codec.from_quantizer.weight").ok_or("lfm3_speech: codec from_quantizer missing")?;
561        let lat = N_CODEBOOKS * FSQ_LEVELS.len();
562        let bins2 = tensor_rows(model, "codec.decoder.lin.weight").ok_or("lfm3_speech: codec lin missing")?;
563        if bins2 != 2 * (N_FFT / 2 + 1) {
564            return Err("lfm3_speech: codec spectrum width differs".into());
565        }
566        let hd = tensor_rows(model, "codec.decoder.lfm.layers.2.self_attn.q_norm.weight").unwrap_or(64);
567        let heads = tensor_rows(model, "codec.decoder.lfm.layers.2.self_attn.q_proj.weight").unwrap_or(ed) / hd;
568        let kv = tensor_rows(model, "codec.decoder.lfm.layers.2.self_attn.k_proj.weight").unwrap_or(ed) / hd;
569        Ok(Self {
570            project_out: (
571                load_proj(model, "codec.quantizer.project_out.weight", qd, lat)?,
572                load_vec(model, "codec.quantizer.project_out.bias", qd)?,
573            ),
574            from_q: (
575                load_proj(model, "codec.from_quantizer.weight", ed, qd)?,
576                load_vec(model, "codec.from_quantizer.bias", ed)?,
577            ),
578            stack: Stack::load(model, "codec.decoder.lfm", ed, heads, kv, 1e-5, 1e6, Some(30))?,
579            lin: (
580                load_proj(model, "codec.decoder.lin.weight", bins2, ed)?,
581                load_vec(model, "codec.decoder.lin.bias", bins2)?,
582            ),
583            window: load_vec(model, "codec.decoder.istft.window", N_FFT)?,
584            pool,
585        })
586    }
587
588    /// 24 kHz samples of `frames` (1920 a frame).
589    pub fn decode(&self, frames: &[[u32; N_CODEBOOKS]]) -> Vec<f32> {
590        use rustfft::{FftPlanner, num_complex::Complex};
591        let pool = self.pool.as_deref();
592        let nf = frames.len();
593        if nf == 0 {
594            return Vec::new();
595        }
596        let lat_w = N_CODEBOOKS * FSQ_LEVELS.len();
597        let mut lat = vec![0f32; nf * lat_w];
598        let basis = [1u32, 8, 56, 336];
599        for (f, codes) in frames.iter().enumerate() {
600            for (i, &c) in codes.iter().enumerate() {
601                for (j, (&lv, &b)) in FSQ_LEVELS.iter().zip(&basis).enumerate() {
602                    let half = (lv / 2) as f32;
603                    lat[f * lat_w + i * 4 + j] = (((c / b) % lv) as f32 - half) / half;
604                }
605            }
606        }
607        let qd = self.project_out.0.rows;
608        let mut q = vec![0f32; nf * qd];
609        self.project_out.0.matmat(&lat, nf, &mut q, pool);
610        add_bias(&mut q, &self.project_out.1);
611        let ed = self.from_q.0.rows;
612        let mut e = vec![0f32; nf * ed];
613        self.from_q.0.matmat(&q, nf, &mut e, pool);
614        add_bias(&mut e, &self.from_q.1);
615        let n = nf * FRAME_STACK;
616        let mut x = vec![0f32; n * ed];
617        for t in 0..n {
618            x[t * ed..(t + 1) * ed].copy_from_slice(&e[(t / FRAME_STACK) * ed..][..ed]);
619        }
620        self.stack.forward(&mut x, pool);
621        let bins = N_FFT / 2 + 1;
622        let mut spec = vec![0f32; n * 2 * bins];
623        self.lin.0.matmat(&x, n, &mut spec, pool);
624        add_bias(&mut spec, &self.lin.1);
625        let ifft = FftPlanner::<f64>::new().plan_fft_inverse(N_FFT);
626        let out_len = (n - 1) * HOP + N_FFT;
627        let mut audio = vec![0f64; out_len];
628        let mut env = vec![0f64; out_len];
629        let mut buf = vec![Complex::new(0.0, 0.0); N_FFT];
630        for t in 0..n {
631            let row = &spec[t * 2 * bins..(t + 1) * 2 * bins];
632            for k in 0..bins {
633                let mag = (row[k] as f64).exp();
634                let ph = row[bins + k] as f64;
635                let v = Complex::new(mag * ph.cos(), mag * ph.sin());
636                buf[k] = if k == 0 || k == N_FFT / 2 { Complex::new(v.re, 0.0) } else { v };
637                if k > 0 && k < N_FFT / 2 {
638                    buf[N_FFT - k] = v.conj();
639                }
640            }
641            ifft.process(&mut buf);
642            for (i, b) in buf.iter().enumerate() {
643                let w = self.window[i] as f64;
644                audio[t * HOP + i] += b.re / N_FFT as f64 * w;
645                env[t * HOP + i] += w * w;
646            }
647        }
648        let trim = (N_FFT - HOP) / 2;
649        audio[trim..out_len - trim]
650            .iter()
651            .zip(&env[trim..out_len - trim])
652            .map(|(&a, &w)| (a / w) as f32)
653            .collect()
654    }
655}
656
657// ───────────────────────────── generation ─────────────────────────────
658
659/// A speech turn: the frames and the text tokens around them.
660pub struct SpeechOut {
661    pub frames: Vec<[u32; N_CODEBOOKS]>,
662    pub text_ids: Vec<u32>,
663}
664
665/// Whether the file can speak (depth decoder and codec).
666pub fn can_speak(model: &CmfModel) -> bool {
667    has_tensor(model, "depth_decoder.depth_linear.weight") && has_tensor(model, "codec.decoder.lin.weight")
668}
669
670/// Generate after a prompt that ends with `<|audio_start|>` (speech, then
671/// text until the turn ends) or `<|mixed_start|>` (Liquid's interleaved
672/// turn: six text tokens, twelve speech frames, … until `<|text_end|>`,
673/// then speech until the end-of-audio frame), at most `max_steps` steps.
674/// Text is greedy; speech is sampled with `s`.
675pub fn speak(
676    pipeline: &mut Pipeline,
677    depth: &DepthDecoder,
678    ids: &[u32],
679    rows: Option<&[f32]>,
680    max_steps: usize,
681    s: &AudioSampling,
682    seed: u64,
683) -> Result<SpeechOut, String> {
684    const N_TEXT: usize = 6;
685    const N_AUDIO: usize = 12;
686    let id = |t: &str| pipeline.tokenizer.token_to_id(t);
687    let audio_start = id("<|audio_start|>").ok_or("tokenizer lacks <|audio_start|>")?;
688    let (mixed_start, text_end, im_end) = (id("<|mixed_start|>"), id("<|text_end|>"), id("<|im_end|>"));
689    #[derive(PartialEq, Clone, Copy)]
690    enum Mode {
691        Text,
692        Audio,
693        Mixed,
694    }
695    let mut mode = match ids.last() {
696        Some(&t) if t == audio_start => Mode::Audio,
697        Some(&t) if Some(t) == mixed_start => Mode::Mixed,
698        _ => Mode::Text,
699    };
700    let (mut slots_left, mut speaking, mut text_done) = (N_TEXT, false, false);
701    let mut rng = SplitMix64::new(seed);
702    let mut h = pipeline.speech_prefill(ids, rows)?;
703    let mut pos = ids.len();
704    let mut out = SpeechOut { frames: Vec::new(), text_ids: Vec::new() };
705    // CMF_LFM3_SPEECH_PROF: where a turn's time goes.
706    let prof = std::env::var_os("CMF_LFM3_SPEECH_PROF").is_some();
707    let (mut t_depth, mut t_lm, mut t_head) = (0f64, 0f64, 0f64);
708    for _ in 0..max_steps {
709        let interleaving = mode == Mode::Mixed;
710        let t0 = std::time::Instant::now();
711        let row = if mode == Mode::Audio || (interleaving && speaking) {
712            let codes = depth.sample_frame(&h, s, &out.frames, &mut rng);
713            t_depth += t0.elapsed().as_secs_f64();
714            if codes.contains(&AUDIO_EOS) {
715                mode = Mode::Text;
716                depth.frame_row(&[AUDIO_EOS; N_CODEBOOKS])
717            } else {
718                out.frames.push(codes);
719                depth.frame_row(&codes)
720            }
721        } else {
722            let logits = pipeline.speech_logits(&h);
723            t_head += t0.elapsed().as_secs_f64();
724            let t = sample(&logits, 0.0, 0, &mut rng);
725            if Some(t) == text_end {
726                if !interleaving {
727                    break;
728                }
729                // The text of the interleaved turn is over; its speech runs on.
730                text_done = true;
731                speaking = true;
732            } else if Some(t) == im_end || pipeline.tokenizer.is_eos(t) {
733                break;
734            } else if !interleaving && t == audio_start {
735                mode = Mode::Audio;
736            } else if !interleaving && Some(t) == mixed_start {
737                mode = Mode::Mixed;
738                (slots_left, speaking, text_done) = (N_TEXT, false, false);
739            }
740            out.text_ids.push(t);
741            pipeline.embed_id(t)
742        };
743        if interleaving && !text_done {
744            slots_left -= 1;
745            if slots_left == 0 {
746                speaking = !speaking;
747                slots_left = if speaking { N_AUDIO } else { N_TEXT };
748            }
749        }
750        let t1 = std::time::Instant::now();
751        h = pipeline.speech_step(&row, pos);
752        t_lm += t1.elapsed().as_secs_f64();
753        pos += 1;
754    }
755    if prof {
756        eprintln!(
757            "speech: {} frames, {} text tokens | depth decoder {:.0} ms, language model {:.0} ms, text head {:.0} ms",
758            out.frames.len(),
759            out.text_ids.len(),
760            t_depth * 1e3,
761            t_lm * 1e3,
762            t_head * 1e3
763        );
764    }
765    Ok(out)
766}
767
768/// 16-bit PCM mono WAV at `rate`.
769pub fn write_wav(path: &std::path::Path, samples: &[f32], rate: u32) -> std::io::Result<()> {
770    use std::io::Write;
771    let bytes = (samples.len() * 2) as u32;
772    let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
773    f.write_all(b"RIFF")?;
774    f.write_all(&(36 + bytes).to_le_bytes())?;
775    f.write_all(b"WAVEfmt ")?;
776    f.write_all(&16u32.to_le_bytes())?;
777    f.write_all(&1u16.to_le_bytes())?;
778    f.write_all(&1u16.to_le_bytes())?;
779    f.write_all(&rate.to_le_bytes())?;
780    f.write_all(&(rate * 2).to_le_bytes())?;
781    f.write_all(&2u16.to_le_bytes())?;
782    f.write_all(&16u16.to_le_bytes())?;
783    f.write_all(b"data")?;
784    f.write_all(&bytes.to_le_bytes())?;
785    for &v in samples {
786        f.write_all(&((v.clamp(-1.0, 1.0) * 32767.0) as i16).to_le_bytes())?;
787    }
788    f.flush()
789}