1use crate::audiovae::AudioVae;
20use crate::mmh3::{Layout, MiniMaxH3, time_shift_sigma};
21use crate::qwen3te::{ImageSpan, Qwen3Encoder};
22use crate::qwen3vis::{self, VisionTower};
23use crate::sampler::SplitMix64;
24use crate::tokenizer::Tokenizer;
25use crate::vae3d::VideoVae;
26use crate::vae3d::VideoVaeEncoder;
27use std::path::Path;
28use std::sync::Arc;
29
30pub const FPS: usize = 24;
31pub const AUDIO_LATENT_FPS: usize = 40;
32
33pub struct AnimParams {
34 pub width: usize,
35 pub height: usize,
36 pub frames: usize,
38 pub steps: usize,
39 pub seed: u64,
40 pub stock_sampler: bool,
43 pub max_tokens: usize,
44 pub first_frame: Option<(Vec<f32>, usize, usize)>,
47 pub last_frame: Option<(Vec<f32>, usize, usize)>,
48 pub lora: Option<String>,
50 pub lora_strength: f32,
51}
52
53const VISION_START: u32 = 151_652;
55const VISION_END: u32 = 151_653;
56
57pub fn fit_to_canvas(
61 rgb: &[f32],
62 h: usize,
63 w: usize,
64 out_h: usize,
65 out_w: usize,
66 crop: bool,
67) -> Vec<f32> {
68 let (sx0, sy0, sw, sh) = if crop {
71 let (tw, th) = (out_w as f64, out_h as f64);
72 let scale = (w as f64 / tw).min(h as f64 / th);
73 let (cw, ch) = ((tw * scale).round() as usize, (th * scale).round() as usize);
74 ((w - cw) / 2, (h - ch) / 2, cw.max(1), ch.max(1))
75 } else {
76 (0, 0, w, h)
77 };
78 let mut out = vec![0f32; 3 * out_h * out_w];
79 for c in 0..3 {
80 for y in 0..out_h {
81 let sy = ((y as f64 + 0.5) * sh as f64 / out_h as f64 - 0.5).max(0.0);
82 let y0 = sy.floor() as usize;
83 let y1 = (y0 + 1).min(sh - 1);
84 let fy = (sy - y0 as f64) as f32;
85 for x in 0..out_w {
86 let sx = ((x as f64 + 0.5) * sw as f64 / out_w as f64 - 0.5).max(0.0);
87 let x0 = sx.floor() as usize;
88 let x1 = (x0 + 1).min(sw - 1);
89 let fx = (sx - x0 as f64) as f32;
90 let p = |yy: usize, xx: usize| rgb[(c * h + sy0 + yy) * w + sx0 + xx];
91 let top = p(y0, x0) * (1.0 - fx) + p(y0, x1) * fx;
92 let bot = p(y1, x0) * (1.0 - fx) + p(y1, x1) * fx;
93 out[(c * out_h + y) * out_w + x] = top * (1.0 - fy) + bot * fy;
94 }
95 }
96 }
97 out
98}
99
100impl Default for AnimParams {
101 fn default() -> Self {
102 Self {
103 width: 512,
104 height: 288,
105 frames: 39,
106 steps: 4,
107 seed: 42,
108 stock_sampler: false,
109 max_tokens: 512,
110 first_frame: None,
111 last_frame: None,
112 lora: None,
113 lora_strength: 1.0,
114 }
115 }
116}
117
118pub struct Anim {
121 pub rgb: Vec<f32>,
122 pub frames: usize,
123 pub height: usize,
124 pub width: usize,
125 pub audio: Vec<f32>,
126 pub samples: usize,
127 pub sample_rate: usize,
128}
129
130pub fn align_frames(n: usize) -> usize {
133 let mut n = n.max(5);
134 while n % 17 != 5 {
135 n += 1;
136 }
137 n
138}
139
140pub fn video_latent_t(frames: usize) -> usize {
141 if frames <= 5 {
142 2
143 } else {
144 (frames - 5) / 17 * 5 + 2
145 }
146}
147
148pub fn temporal_shape(len: usize) -> (usize, usize, usize) {
150 let frames = align_frames(len);
151 let audio_t = ((frames as f64 / FPS as f64) * AUDIO_LATENT_FPS as f64).round() as usize;
152 (frames, video_latent_t(frames), audio_t)
153}
154
155pub fn sigmas(steps: usize, shift: f64) -> Vec<f64> {
158 let table = 1000usize;
159 let mut out: Vec<f64> = (0..steps)
160 .map(|x| {
161 let idx = table - 1 - x * table / steps;
162 let t = (idx + 1) as f64 / table as f64;
163 shift * t / (1.0 + (shift - 1.0) * t)
164 })
165 .collect();
166 out.push(0.0);
167 out
168}
169
170pub fn gauss_pub(n: usize, seed: u64) -> Vec<f32> {
172 gauss(n, seed)
173}
174
175fn gauss(n: usize, seed: u64) -> Vec<f32> {
176 let mut rng = SplitMix64::new(seed);
177 let mut u = || (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
178 let mut out = Vec::with_capacity(n);
179 while out.len() < n {
180 let (a, b) = (u().max(1e-300), u());
181 let r = (-2.0 * a.ln()).sqrt();
182 let ang = 2.0 * std::f64::consts::PI * b;
183 out.push((r * ang.cos()) as f32);
184 if out.len() < n {
185 out.push((r * ang.sin()) as f32);
186 }
187 }
188 out
189}
190
191pub fn generate(
193 path: &Path,
194 prompt: &str,
195 p: &AnimParams,
196 mut progress: impl FnMut(&str, usize, usize),
197) -> Result<Anim, String> {
198 if p.width % 32 != 0 || p.height % 32 != 0 {
199 return Err("width/height must be multiples of 32".into());
200 }
201 let use_gpu = match std::env::var("CMF_MMH3_GPU").ok().as_deref() {
209 Some("1") => true,
210 Some("0") => false,
211 _ => mmh3_gpu_parity_probe(path).unwrap_or(false),
212 };
213 if use_gpu {
214 generate_inner(path, prompt, p, &mut progress)
215 } else {
216 crate::gpu::cpu_scope(|| generate_inner(path, prompt, p, &mut progress))
217 }
218}
219
220fn mmh3_gpu_parity_probe(path: &Path) -> Result<bool, String> {
227 let model = Arc::new(
228 cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
229 );
230 let Some(idx) = model.tensors.iter().position(|t| {
231 t.name.starts_with("dit.")
232 && t.name.ends_with("attn.qkv_proj.weight")
233 && t.dtype == cortiq_core::TensorDtype::Q4TiledP
234 }) else {
235 tracing::info!("mmh3 GPU parity probe: no q4tp qkv tensor — host path");
236 return Ok(false);
237 };
238 let entry = &model.tensors[idx];
239 let (rows, cols) = (entry.shape[0], entry.shape[1]);
240 let b = 64usize;
241 let mut xs = vec![0f32; b * cols];
242 for (i, v) in xs.iter_mut().enumerate() {
243 let base = ((i * 37 + 11) % 1009) as f32 / 1009.0 - 0.5;
244 *v = base * if i % 7 == 0 { 2000.0 } else { 2.0 };
245 }
246 let mut gpu = vec![0f32; b * rows];
247 if std::env::var("CMF_GPU_DEBUG").is_ok() {
248 eprintln!(
249 "mmh3 probe env: CMF_GPU={:?} enabled={} avail={}",
250 std::env::var("CMF_GPU").ok(),
251 crate::gpu::enabled(),
252 crate::gpu::backend_available(),
253 );
254 }
255 if !crate::gpu::q4tp_matmat(&model, idx, &xs, b, rows, cols, &mut gpu) {
256 tracing::info!("mmh3 GPU parity probe: q4tp_matmat refused ({rows}x{cols}) — host path");
257 if std::env::var("CMF_GPU_DEBUG").is_ok() {
258 eprintln!("mmh3 probe: q4tp_matmat refused {rows}x{cols}");
259 }
260 return Ok(false);
261 }
262 let host = {
263 let name = entry.name.clone();
264 let proj = crate::dit::Proj::from_model(&model, &name)?;
265 let mut out = vec![0f32; b * rows];
266 crate::gpu::cpu_scope(|| proj.matmat(&xs, b, &mut out, None));
267 out
268 };
269 let mut num = 0f64;
270 let mut den = 0f64;
271 for (g, h) in gpu.iter().zip(&host) {
272 num += ((g - h) as f64).powi(2);
273 den += (*h as f64).powi(2);
274 }
275 let rel = (num / den.max(1e-30)).sqrt();
276 let ok = rel < 1e-2;
277 tracing::info!(
278 "mmh3 GPU parity probe: rel rms {rel:.2e} → {}",
279 if ok { "device" } else { "host" }
280 );
281 if std::env::var("CMF_GPU_DEBUG").is_ok() {
282 eprintln!("mmh3 GPU parity probe: rel rms {rel:.2e}");
283 }
284 Ok(ok)
285}
286
287fn generate_inner(
288 path: &Path,
289 prompt: &str,
290 p: &AnimParams,
291 progress: &mut dyn FnMut(&str, usize, usize),
292) -> Result<Anim, String> {
293 let model = Arc::new(
294 cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
295 );
296 let (frames_total, latent_t, audio_t) = temporal_shape(p.frames);
297 let (lat_h, lat_w) = (p.height / 16, p.width / 16);
298
299 let t_stage = std::time::Instant::now();
306 let mut marks: Vec<(&str, f32)> = Vec::new();
307 let lap = |marks: &mut Vec<(&'static str, f32)>, name: &'static str| {
308 let prev: f32 = marks.iter().map(|(_, v)| v).sum();
309 marks.push((name, t_stage.elapsed().as_secs_f32() - prev));
310 };
311 let vocab = model
312 .vocab
313 .as_deref()
314 .ok_or("packaged .cmf has no embedded tokenizer")?;
315 let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
316 let keyframes: Vec<(&(Vec<f32>, usize, usize), usize)> = p
320 .first_frame
321 .iter()
322 .map(|f| (f, 0usize))
323 .chain(p.last_frame.iter().map(|f| (f, frames_total - 1)))
324 .collect();
325 let mut ids: Vec<u32> = Vec::new();
326 let mut spans: Vec<ImageSpan> = Vec::new();
327 let mut embeds: Vec<Vec<f32>> = Vec::new();
328 let mut deepstack: Vec<Vec<f32>> = Vec::new();
329 let mut cond: Vec<Vec<f32>> = Vec::new();
330 let mut tags: Vec<u8> = Vec::new();
331
332 if !keyframes.is_empty() {
333 let tower = VisionTower::from_cmf(&model)?;
334 let te_only = std::env::var("CMF_TE_ONLY").as_deref() == Ok("1");
338 let venc = if te_only {
339 None
340 } else {
341 Some(VideoVaeEncoder::from_cmf(&model)?)
342 };
343 for (i, (frame, _)) in keyframes.iter().enumerate() {
344 let (src, sh, sw) = *frame;
345 let fitted = fit_to_canvas(src, *sh, *sw, p.height, p.width, i > 0);
348 if let Some(venc) = &venc {
349 let (z, _, _) = venc.encode_frame(
350 &fitted.iter().map(|&v| v * 2.0 - 1.0).collect::<Vec<_>>(),
351 p.height,
352 p.width,
353 );
354 cond.push(z);
355 }
356
357 for t in tok.encode(&format!("<Picture {}>: ", i + 1)) {
358 ids.push(t);
359 tags.push(1);
360 }
361 let (patches, gh, gw) = qwen3vis::preprocess(
362 &fitted,
363 p.height,
364 p.width,
365 tower.patch_size,
366 tower.temporal_patch,
367 tower.merge,
368 );
369 let (merged, deep) = tower.forward(&patches, gh, gw);
370 let n_img = merged.len() / tower.out_hidden;
371 ids.push(VISION_START);
374 tags.push(0);
375 let start = ids.len();
376 for _ in 0..n_img {
377 ids.push(VISION_START); tags.push(0);
379 }
380 ids.push(VISION_END);
381 tags.push(0);
382 spans.push(ImageSpan {
383 start,
384 len: n_img,
385 merged_h: gh / tower.merge,
386 merged_w: gw / tower.merge,
387 });
388 embeds.push(merged);
389 if deepstack.is_empty() {
390 deepstack = deep;
391 } else {
392 for (a, b) in deepstack.iter_mut().zip(deep) {
393 a.extend_from_slice(&b);
394 }
395 }
396 }
397 }
398 for t in tok.encode(prompt) {
399 ids.push(t);
400 tags.push(1);
401 }
402 if ids.is_empty() {
403 ids.push(151643); tags.push(1);
405 }
406 ids.truncate(p.max_tokens);
407 tags.truncate(ids.len());
408 lap(&mut marks, "prepare");
409 crate::gpu::mm_kill_arm(false);
415 progress("encode", 0, 1);
416 let states = {
417 let enc = Qwen3Encoder::from_cmf(&model)?;
418 enc.encode_with_images(&ids, &spans, &embeds, &deepstack)
419 };
420 if let Ok(p) = std::env::var("CMF_TE_DUMP") {
425 let w = states.len() / ids.len().max(1);
426 let mut b = Vec::with_capacity(16 + states.len() * 4);
427 b.extend_from_slice(&(ids.len() as u64).to_le_bytes());
428 b.extend_from_slice(&(w as u64).to_le_bytes());
429 for v in &states {
430 b.extend_from_slice(&v.to_le_bytes());
431 }
432 std::fs::write(&p, &b).map_err(|e| format!("CMF_TE_DUMP {p}: {e}"))?;
433 eprintln!("te dump: {} tokens x {w} -> {p}", ids.len());
434 }
435 progress("encode", 1, 1);
436 lap(&mut marks, "text encode");
437 crate::gpu::mm_kill_arm(true);
438 if std::env::var("CMF_TE_ONLY").as_deref() == Ok("1") {
441 return Err("CMF_TE_ONLY: encode dumped, render skipped".into());
442 }
443 {
449 let dropped = model.advise_done(|n| n.starts_with("model.") || n.starts_with("vis."));
450 if dropped > 0 {
451 tracing::info!(
452 "encoder pages released after prompt encode: {} MB",
453 dropped / (1024 * 1024)
454 );
455 }
456 }
457
458 let (video, audio) = {
460 let bank = match p.lora.as_deref() {
465 None => None,
466 Some(path) => {
467 let k = crate::ltxlora::LoraBank::load(std::path::Path::new(path), p.lora_strength)?;
468 Some(k)
469 }
470 };
471 let dit = MiniMaxH3::from_cmf_lora(&model, bank.as_ref())?;
472 if let Some(k) = &bank {
473 let bound = dit.lora_bound();
474 let mut skipped: std::collections::BTreeMap<String, usize> = Default::default();
478 for name in k.keys() {
479 if !dit.lora_binds(name) {
480 let fam = name
481 .rsplit_once('.')
482 .map(|(_, t)| {
483 let head = name.split('.').next().unwrap_or("");
484 format!("{head}…{t}")
485 })
486 .unwrap_or_else(|| name.to_string());
487 *skipped.entry(fam).or_default() += 1;
488 }
489 }
490 let tail = if skipped.is_empty() {
491 String::new()
492 } else {
493 let parts: Vec<String> =
494 skipped.iter().map(|(k, v)| format!("{k} ×{v}")).collect();
495 format!("; not applied: {}", parts.join(", "))
496 };
497 tracing::info!(
498 "lora: rank {}, {} branches, {} bound at strength {}{}",
499 k.rank(),
500 k.len(),
501 bound,
502 p.lora_strength,
503 tail
504 );
505 println!(
506 "lora: rank {}, {}/{} branches bound{}",
507 k.rank(),
508 bound,
509 k.len(),
510 tail
511 );
512 }
513 let kf: Vec<(usize, usize)> = keyframes
514 .iter()
515 .map(|&(_, idx)| (idx, frames_total))
516 .collect();
517 let layout = if kf.is_empty() {
518 Layout::t2va(ids.len(), latent_t, lat_h, lat_w, audio_t)
519 } else {
520 Layout::fl2va(ids.len(), latent_t, lat_h, lat_w, audio_t, &kf, &tags)
521 };
522 let text = dit.refine_text(&states, ids.len());
523 let mut v = gauss(dit.latents_dim * latent_t * lat_h * lat_w, p.seed);
524 let mut a = gauss(dit.audio_dim * 2 * audio_t, p.seed ^ 0x9E37_79B9_7F4A_7C15);
525 let sg = sigmas(p.steps, dit.shift_video);
526 let prof = std::env::var_os("CMF_ANIM_PROF").is_some();
530 let rms = |x: &[f32]| {
531 (x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64).sqrt()
532 };
533 if prof {
534 eprintln!(
535 " text {} tok, refined rms {:.4}, sigmas {:?}",
536 ids.len(),
537 rms(&text),
538 sg.iter()
539 .map(|v| (v * 1e4).round() / 1e4)
540 .collect::<Vec<_>>()
541 );
542 }
543 for i in 0..p.steps {
544 let (sv, sv_n) = (sg[i], sg[i + 1]);
545 let (dv, da) = dit.forward(&layout, &text, &v, &a, sv, &cond);
546 let step_v = (sv_n - sv) as f32;
547 for (x, &d) in v.iter_mut().zip(&dv) {
548 *x += step_v * d;
549 }
550 let step_a = if p.stock_sampler {
551 step_v
552 } else {
553 (time_shift_sigma(sv_n, dit.shift_video, dit.shift_audio)
554 - time_shift_sigma(sv.max(1e-6), dit.shift_video, dit.shift_audio))
555 as f32
556 };
557 for (x, &d) in a.iter_mut().zip(&da) {
558 *x += step_a * d;
559 }
560 if prof {
561 eprintln!(
562 " step {i}: sv {sv:.4}->{sv_n:.4} v_vel {:.4} a_vel {:.4} | video {:.4} audio {:.4}",
563 rms(&dv),
564 rms(&da),
565 rms(&v),
566 rms(&a)
567 );
568 }
569 progress("denoise", i + 1, p.steps);
570 }
571 if let Some(rep) = dit.lora_report() {
572 eprint!("{rep}");
573 }
574 (v, a)
575 };
576
577 lap(&mut marks, "denoise");
578 progress("video vae", 0, 1);
580 let (rgb, out_frames) = {
581 let vae = VideoVae::from_cmf(&model)?;
582 vae.decode(&video, latent_t, lat_h, lat_w)
583 };
584 progress("video vae", 1, 1);
585 lap(&mut marks, "video vae");
586 progress("audio vae", 0, 1);
587 let (wave, samples, sr) = {
588 let vae = AudioVae::from_cmf(&model)?;
589 let c = audio.len() / (2 * audio_t);
590 let (w, n) = vae.decode(&audio, c, audio_t);
591 (w, n, vae.sample_rate)
592 };
593 progress("audio vae", 1, 1);
594 lap(&mut marks, "audio vae");
595 tracing::info!(
596 "stages: {}",
597 marks
598 .iter()
599 .map(|(n, v)| format!("{n} {v:.1}s"))
600 .collect::<Vec<_>>()
601 .join(" · ")
602 );
603 #[cfg(target_os = "macos")]
607 if std::env::var("CMF_METAL_MMPROF").is_ok() {
608 use std::sync::atomic::Ordering::Relaxed;
609 let n = crate::gpu_metal::MM_N.load(Relaxed);
610 eprintln!(
611 " q4tp mm x{n}: upload {:.1}s · submit+wait {:.1}s · readback {:.1}s",
612 crate::gpu_metal::MM_UP.load(Relaxed) as f64 / 1e6,
613 crate::gpu_metal::MM_GPU.load(Relaxed) as f64 / 1e6,
614 crate::gpu_metal::MM_DN.load(Relaxed) as f64 / 1e6,
615 );
616 }
617
618 let keep = out_frames.min(frames_total);
622 Ok(Anim {
623 rgb: trim_frames(&rgb, out_frames, keep, p.height, p.width),
624 frames: keep,
625 height: p.height,
626 width: p.width,
627 audio: wave,
628 samples,
629 sample_rate: sr,
630 })
631}
632
633fn trim_frames(rgb: &[f32], have: usize, keep: usize, h: usize, w: usize) -> Vec<f32> {
634 if keep == have {
635 return rgb.to_vec();
636 }
637 let mut out = vec![0f32; 3 * keep * h * w];
638 for c in 0..3 {
639 let s = c * have * h * w;
640 let d = c * keep * h * w;
641 out[d..d + keep * h * w].copy_from_slice(&rgb[s..s + keep * h * w]);
642 }
643 out
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
651 fn the_four_step_schedule_is_the_references() {
652 let s = sigmas(4, 12.0);
653 let want = [1.0, 0.972_973, 0.923_077, 0.8, 0.0];
654 assert_eq!(s.len(), want.len());
655 for (g, w) in s.iter().zip(&want) {
656 assert!((g - w).abs() < 1e-6, "{s:?}");
657 }
658 }
659
660 #[test]
661 fn a_stretch_keeps_the_corners_and_a_crop_takes_the_middle() {
662 let (h, w) = (2usize, 4usize);
665 let mut rgb = vec![0f32; 3 * h * w];
666 for c in 0..3 {
667 for y in 0..h {
668 for x in 0..w {
669 rgb[(c * h + y) * w + x] = x as f32 / (w - 1) as f32;
670 }
671 }
672 }
673 let s = fit_to_canvas(&rgb, h, w, 4, 4, false);
675 assert!((s[0] - 0.0).abs() < 1e-6, "left edge");
676 assert!((s[3] - 1.0).abs() < 1e-6, "right edge");
677 let c = fit_to_canvas(&rgb, h, w, 4, 4, true);
680 let (lo, hi) = c[..16]
681 .iter()
682 .fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
683 assert!(lo > 0.05, "crop kept the left edge: {lo}");
684 assert!(hi < 0.95, "crop kept the right edge: {hi}");
685 }
686
687 #[test]
688 fn frame_counts_snap_to_the_models_grid() {
689 assert_eq!(align_frames(1), 5);
691 assert_eq!(align_frames(39), 39);
692 assert_eq!(align_frames(41), 56);
693 assert_eq!(align_frames(124), 124);
694 assert_eq!(video_latent_t(124), 37);
695 assert_eq!(video_latent_t(39), 12);
696 let (f, lt, at) = temporal_shape(124);
697 assert_eq!((f, lt, at), (124, 37, 207));
698 }
699}