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 mid_frames: Vec<((Vec<f32>, usize, usize), usize)>,
53 pub lora: Option<String>,
55 pub lora_strength: f32,
56 pub upscale: Option<String>,
61 pub upscale_by: f32,
62 pub stream_chunk: usize,
69 pub stream_sink: usize,
70 pub stream_window: usize,
71}
72
73const VISION_START: u32 = 151_652;
75const VISION_END: u32 = 151_653;
76
77pub fn fit_to_canvas(
81 rgb: &[f32],
82 h: usize,
83 w: usize,
84 out_h: usize,
85 out_w: usize,
86 crop: bool,
87) -> Vec<f32> {
88 let (sx0, sy0, sw, sh) = if crop {
91 let (tw, th) = (out_w as f64, out_h as f64);
92 let scale = (w as f64 / tw).min(h as f64 / th);
93 let (cw, ch) = ((tw * scale).round() as usize, (th * scale).round() as usize);
94 ((w - cw) / 2, (h - ch) / 2, cw.max(1), ch.max(1))
95 } else {
96 (0, 0, w, h)
97 };
98 let mut out = vec![0f32; 3 * out_h * out_w];
99 for c in 0..3 {
100 for y in 0..out_h {
101 let sy = ((y as f64 + 0.5) * sh as f64 / out_h as f64 - 0.5).max(0.0);
102 let y0 = sy.floor() as usize;
103 let y1 = (y0 + 1).min(sh - 1);
104 let fy = (sy - y0 as f64) as f32;
105 for x in 0..out_w {
106 let sx = ((x as f64 + 0.5) * sw as f64 / out_w as f64 - 0.5).max(0.0);
107 let x0 = sx.floor() as usize;
108 let x1 = (x0 + 1).min(sw - 1);
109 let fx = (sx - x0 as f64) as f32;
110 let p = |yy: usize, xx: usize| rgb[(c * h + sy0 + yy) * w + sx0 + xx];
111 let top = p(y0, x0) * (1.0 - fx) + p(y0, x1) * fx;
112 let bot = p(y1, x0) * (1.0 - fx) + p(y1, x1) * fx;
113 out[(c * out_h + y) * out_w + x] = top * (1.0 - fy) + bot * fy;
114 }
115 }
116 }
117 out
118}
119
120impl Default for AnimParams {
121 fn default() -> Self {
122 Self {
123 width: 512,
124 height: 288,
125 frames: 39,
126 steps: 4,
127 seed: 42,
128 stock_sampler: false,
129 max_tokens: 512,
130 first_frame: None,
131 last_frame: None,
132 mid_frames: Vec::new(),
133 lora: None,
134 lora_strength: 1.0,
135 upscale: None,
136 upscale_by: 2.0,
137 stream_chunk: 0,
138 stream_sink: 2,
139 stream_window: 2,
140 }
141 }
142}
143
144pub struct Anim {
147 pub rgb: Vec<f32>,
148 pub frames: usize,
149 pub height: usize,
150 pub width: usize,
151 pub audio: Vec<f32>,
152 pub samples: usize,
153 pub sample_rate: usize,
154}
155
156pub fn align_frames(n: usize) -> usize {
159 let mut n = n.max(5);
160 while n % 17 != 5 {
161 n += 1;
162 }
163 n
164}
165
166pub fn video_latent_t(frames: usize) -> usize {
167 if frames <= 5 {
168 2
169 } else {
170 (frames - 5) / 17 * 5 + 2
171 }
172}
173
174pub fn temporal_shape(len: usize) -> (usize, usize, usize) {
176 let frames = align_frames(len);
177 let audio_t = ((frames as f64 / FPS as f64) * AUDIO_LATENT_FPS as f64).round() as usize;
178 (frames, video_latent_t(frames), audio_t)
179}
180
181pub fn sigmas(steps: usize, shift: f64) -> Vec<f64> {
184 let table = 1000usize;
185 let mut out: Vec<f64> = (0..steps)
186 .map(|x| {
187 let idx = table - 1 - x * table / steps;
188 let t = (idx + 1) as f64 / table as f64;
189 shift * t / (1.0 + (shift - 1.0) * t)
190 })
191 .collect();
192 out.push(0.0);
193 out
194}
195
196pub fn gauss_pub(n: usize, seed: u64) -> Vec<f32> {
198 gauss(n, seed)
199}
200
201fn gauss(n: usize, seed: u64) -> Vec<f32> {
202 let mut rng = SplitMix64::new(seed);
203 let mut u = || (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
204 let mut out = Vec::with_capacity(n);
205 while out.len() < n {
206 let (a, b) = (u().max(1e-300), u());
207 let r = (-2.0 * a.ln()).sqrt();
208 let ang = 2.0 * std::f64::consts::PI * b;
209 out.push((r * ang.cos()) as f32);
210 if out.len() < n {
211 out.push((r * ang.sin()) as f32);
212 }
213 }
214 out
215}
216
217pub fn generate(
219 path: &Path,
220 prompt: &str,
221 p: &AnimParams,
222 mut progress: impl FnMut(&str, usize, usize),
223) -> Result<Anim, String> {
224 if p.width % 32 != 0 || p.height % 32 != 0 {
225 return Err("width/height must be multiples of 32".into());
226 }
227 let use_gpu = match std::env::var("CMF_MMH3_GPU").ok().as_deref() {
235 Some("1") => true,
236 Some("0") => false,
237 _ => mmh3_gpu_parity_probe(path).unwrap_or(false),
238 };
239 if use_gpu {
240 generate_inner(path, prompt, p, &mut progress)
241 } else {
242 crate::gpu::cpu_scope(|| generate_inner(path, prompt, p, &mut progress))
243 }
244}
245
246pub(crate) fn mmh3_gpu_parity_probe(path: &Path) -> Result<bool, String> {
253 let model = Arc::new(
254 cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
255 );
256 let Some(idx) = model
263 .tensors
264 .iter()
265 .position(|t| t.name.starts_with("dit.") && t.name.ends_with("attn.qkv_proj.weight"))
266 .or_else(|| {
267 model
268 .tensors
269 .iter()
270 .position(|t| t.name.starts_with("te.layers.") && t.name.ends_with("mlp.gate_proj.weight"))
271 })
272 else {
273 tracing::info!("mmh3 GPU parity probe: no qkv weight — host path");
274 return Ok(false);
275 };
276 let entry = &model.tensors[idx];
277 let (rows, cols) = (entry.shape[0], entry.shape[1]);
278 let b = 64usize;
279 let mut xs = vec![0f32; b * cols];
280 for (i, v) in xs.iter_mut().enumerate() {
281 let base = ((i * 37 + 11) % 1009) as f32 / 1009.0 - 0.5;
282 *v = base * if i % 7 == 0 { 2000.0 } else { 2.0 };
283 }
284 let mut gpu = vec![0f32; b * rows];
285 if std::env::var("CMF_GPU_DEBUG").is_ok() {
286 eprintln!(
287 "mmh3 probe env: CMF_GPU={:?} enabled={} avail={}",
288 std::env::var("CMF_GPU").ok(),
289 crate::gpu::enabled(),
290 crate::gpu::backend_available(),
291 );
292 }
293 let qt = crate::qtensor::QTensor::from_model(&model, &entry.name.clone())?;
294 let took = if crate::gpu::cuda_active() {
298 crate::gpu::q4tp_matmat(&model, idx, &xs, b, rows, cols, &mut gpu)
299 } else {
300 qt.device_matmat(&xs, b, &mut gpu)
301 };
302 if !took {
303 tracing::info!(
304 "mmh3 GPU parity probe: {:?} device GEMM refused ({rows}x{cols}) — host path",
305 entry.dtype
306 );
307 if std::env::var("CMF_GPU_DEBUG").is_ok() {
308 eprintln!(
309 "mmh3 probe: device GEMM refused {rows}x{cols} for {:?}",
310 entry.dtype
311 );
312 }
313 return Ok(false);
314 }
315 let host = {
316 let name = entry.name.clone();
317 let proj = crate::dit::Proj::from_model(&model, &name)?;
318 let mut out = vec![0f32; b * rows];
319 crate::gpu::cpu_scope(|| proj.matmat(&xs, b, &mut out, None));
320 out
321 };
322 let mut num = 0f64;
323 let mut den = 0f64;
324 for (g, h) in gpu.iter().zip(&host) {
325 num += ((g - h) as f64).powi(2);
326 den += (*h as f64).powi(2);
327 }
328 let rel = (num / den.max(1e-30)).sqrt();
329 let ok = rel < 1e-2;
330 tracing::info!(
331 "mmh3 GPU parity probe: rel rms {rel:.2e} → {}",
332 if ok { "device" } else { "host" }
333 );
334 if std::env::var("CMF_GPU_DEBUG").is_ok() {
335 eprintln!("mmh3 GPU parity probe: rel rms {rel:.2e}");
336 }
337 Ok(ok)
338}
339
340fn generate_inner(
341 path: &Path,
342 prompt: &str,
343 p: &AnimParams,
344 progress: &mut dyn FnMut(&str, usize, usize),
345) -> Result<Anim, String> {
346 let model = Arc::new(
347 cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
348 );
349 let (frames_total, latent_t, audio_t) = temporal_shape(p.frames);
350 let (lat_h, lat_w) = (p.height / 16, p.width / 16);
351
352 let t_stage = std::time::Instant::now();
359 let mut marks: Vec<(&str, f32)> = Vec::new();
360 let lap = |marks: &mut Vec<(&'static str, f32)>, name: &'static str| {
361 let prev: f32 = marks.iter().map(|(_, v)| v).sum();
362 marks.push((name, t_stage.elapsed().as_secs_f32() - prev));
363 };
364 let vocab = model
365 .vocab
366 .as_deref()
367 .ok_or("packaged .cmf has no embedded tokenizer")?;
368 let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
369 let mut keyframes: Vec<(&(Vec<f32>, usize, usize), usize)> = p
373 .first_frame
374 .iter()
375 .map(|f| (f, 0usize))
376 .chain(
377 p.mid_frames
378 .iter()
379 .map(|(f, i)| (f, (*i).min(frames_total - 1))),
380 )
381 .chain(p.last_frame.iter().map(|f| (f, frames_total - 1)))
382 .collect();
383 keyframes.sort_by_key(|(_, i)| *i);
387 keyframes.dedup_by_key(|(_, i)| *i);
388 let mut ids: Vec<u32> = Vec::new();
389 let mut spans: Vec<ImageSpan> = Vec::new();
390 let mut embeds: Vec<Vec<f32>> = Vec::new();
391 let mut deepstack: Vec<Vec<f32>> = Vec::new();
392 let mut cond: Vec<Vec<f32>> = Vec::new();
393 let mut tags: Vec<u8> = Vec::new();
394
395 if !keyframes.is_empty() {
396 let tower = VisionTower::from_cmf(&model)?;
397 let te_only = std::env::var("CMF_TE_ONLY").as_deref() == Ok("1");
401 let venc = if te_only {
402 None
403 } else {
404 Some(VideoVaeEncoder::from_cmf(&model)?)
405 };
406 for (i, (frame, _)) in keyframes.iter().enumerate() {
407 let (src, sh, sw) = *frame;
408 let fitted = fit_to_canvas(src, *sh, *sw, p.height, p.width, i > 0);
411 if let Some(venc) = &venc {
412 let (z, _, _) = venc.encode_frame(
413 &fitted.iter().map(|&v| v * 2.0 - 1.0).collect::<Vec<_>>(),
414 p.height,
415 p.width,
416 );
417 cond.push(z);
418 }
419
420 for t in tok.encode(&format!("<Picture {}>: ", i + 1)) {
421 ids.push(t);
422 tags.push(1);
423 }
424 let (patches, gh, gw) = qwen3vis::preprocess(
425 &fitted,
426 p.height,
427 p.width,
428 tower.patch_size,
429 tower.temporal_patch,
430 tower.merge,
431 );
432 let (merged, deep) = tower.forward(&patches, gh, gw);
433 let n_img = merged.len() / tower.out_hidden;
434 ids.push(VISION_START);
437 tags.push(0);
438 let start = ids.len();
439 for _ in 0..n_img {
440 ids.push(VISION_START); tags.push(0);
442 }
443 ids.push(VISION_END);
444 tags.push(0);
445 spans.push(ImageSpan {
446 start,
447 len: n_img,
448 merged_h: gh / tower.merge,
449 merged_w: gw / tower.merge,
450 });
451 embeds.push(merged);
452 if deepstack.is_empty() {
453 deepstack = deep;
454 } else {
455 for (a, b) in deepstack.iter_mut().zip(deep) {
456 a.extend_from_slice(&b);
457 }
458 }
459 }
460 }
461 for t in tok.encode(prompt) {
462 ids.push(t);
463 tags.push(1);
464 }
465 if ids.is_empty() {
466 ids.push(151643); tags.push(1);
468 }
469 ids.truncate(p.max_tokens);
470 tags.truncate(ids.len());
471 lap(&mut marks, "prepare");
472 crate::gpu::mm_kill_arm(false);
478 progress("encode", 0, 1);
479 let states = {
480 let enc = Qwen3Encoder::from_cmf(&model)?;
481 enc.encode_with_images(&ids, &spans, &embeds, &deepstack)
482 };
483 if let Ok(p) = std::env::var("CMF_TE_DUMP") {
488 let w = states.len() / ids.len().max(1);
489 let mut b = Vec::with_capacity(16 + states.len() * 4);
490 b.extend_from_slice(&(ids.len() as u64).to_le_bytes());
491 b.extend_from_slice(&(w as u64).to_le_bytes());
492 for v in &states {
493 b.extend_from_slice(&v.to_le_bytes());
494 }
495 std::fs::write(&p, &b).map_err(|e| format!("CMF_TE_DUMP {p}: {e}"))?;
496 eprintln!("te dump: {} tokens x {w} -> {p}", ids.len());
497 }
498 progress("encode", 1, 1);
499 lap(&mut marks, "text encode");
500 crate::gpu::mm_kill_arm(true);
501 if std::env::var("CMF_TE_ONLY").as_deref() == Ok("1") {
504 return Err("CMF_TE_ONLY: encode dumped, render skipped".into());
505 }
506 {
512 let dropped = model.advise_done(|n| n.starts_with("model.") || n.starts_with("vis."));
513 if dropped > 0 {
514 tracing::info!(
515 "encoder pages released after prompt encode: {} MB",
516 dropped / (1024 * 1024)
517 );
518 }
519 }
520 crate::gpu::stage_done(&model);
524
525 let (mut video, audio) = {
527 let bank = match p.lora.as_deref() {
532 None => None,
533 Some(path) => {
534 let k =
535 crate::ltxlora::LoraBank::load(std::path::Path::new(path), p.lora_strength)?;
536 Some(k)
537 }
538 };
539 let dit = MiniMaxH3::from_cmf_lora(&model, bank.as_ref())?;
540 if let Some(k) = &bank {
541 let bound = dit.lora_bound();
542 let mut skipped: std::collections::BTreeMap<String, usize> = Default::default();
546 for name in k.keys() {
547 if !dit.lora_binds(name) {
548 let fam = name
549 .rsplit_once('.')
550 .map(|(_, t)| {
551 let head = name.split('.').next().unwrap_or("");
552 format!("{head}…{t}")
553 })
554 .unwrap_or_else(|| name.to_string());
555 *skipped.entry(fam).or_default() += 1;
556 }
557 }
558 let tail = if skipped.is_empty() {
559 String::new()
560 } else {
561 let parts: Vec<String> = skipped.iter().map(|(k, v)| format!("{k} ×{v}")).collect();
562 format!("; not applied: {}", parts.join(", "))
563 };
564 tracing::info!(
565 "lora: rank {}, {} branches, {} bound at strength {}{}",
566 k.rank(),
567 k.len(),
568 bound,
569 p.lora_strength,
570 tail
571 );
572 println!(
573 "lora: rank {}, {}/{} branches bound{}",
574 k.rank(),
575 bound,
576 k.len(),
577 tail
578 );
579 }
580 let kf: Vec<(usize, usize)> = keyframes
581 .iter()
582 .map(|&(_, idx)| (idx, frames_total))
583 .collect();
584 let layout = if kf.is_empty() {
585 Layout::t2va(ids.len(), latent_t, lat_h, lat_w, audio_t)
586 } else {
587 Layout::fl2va(ids.len(), latent_t, lat_h, lat_w, audio_t, &kf, &tags)
588 };
589 let text = dit.refine_text(&states, ids.len());
590 let mut v = gauss(dit.latents_dim * latent_t * lat_h * lat_w, p.seed);
591 let mut a = gauss(dit.audio_dim * 2 * audio_t, p.seed ^ 0x9E37_79B9_7F4A_7C15);
592 let sg = sigmas(p.steps, dit.shift_video);
593 let mut return_streaming: Option<(Vec<f32>, Vec<f32>)> = None;
594 let prof = std::env::var_os("CMF_ANIM_PROF").is_some();
598 let rms = |x: &[f32]| {
599 (x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64).sqrt()
600 };
601 if prof {
602 eprintln!(
603 " text {} tok, refined rms {:.4}, sigmas {:?}",
604 ids.len(),
605 rms(&text),
606 sg.iter()
607 .map(|v| (v * 1e4).round() / 1e4)
608 .collect::<Vec<_>>()
609 );
610 }
611 if p.stream_chunk > 0 && cond.is_empty() {
621 let hw = lat_h * lat_w / 4 * 4; let _ = hw;
623 let vc = dit.latents_dim;
624 let ac = dit.audio_dim;
625 let vframe = lat_h * lat_w; let mut chunks: Vec<std::ops::Range<usize>> = (0..latent_t)
627 .step_by(p.stream_chunk)
628 .map(|s| s..(s + p.stream_chunk).min(latent_t))
629 .collect();
630 if chunks.len() > 1 {
636 let tail = chunks[chunks.len() - 1].clone();
637 if tail.len() < p.stream_chunk.div_ceil(2) {
638 chunks.pop();
639 let last = chunks.len() - 1;
640 chunks[last].end = tail.end;
641 }
642 }
643 let a_bound = |k: usize| (k * audio_t).div_ceil(latent_t.max(1));
646 let mut v_out = vec![0f32; v.len()];
647 let mut a_out = vec![0f32; a.len()];
648 let gather_v = |src: &[f32], idx: &[usize]| -> Vec<f32> {
649 let mut out = vec![0f32; vc * idx.len() * vframe];
650 for ci in 0..vc {
651 for (n, &k) in idx.iter().enumerate() {
652 let s = (ci * latent_t + k) * vframe;
653 let d = (ci * idx.len() + n) * vframe;
654 out[d..d + vframe].copy_from_slice(&src[s..s + vframe]);
655 }
656 }
657 out
658 };
659 let gather_a = |src: &[f32], idx: &[usize]| -> Vec<f32> {
660 let mut out = vec![0f32; ac * 2 * idx.len()];
661 for ci in 0..ac {
662 for ch in 0..2 {
663 for (n, &i) in idx.iter().enumerate() {
664 out[(ci * 2 + ch) * idx.len() + n] = src[(ci * 2 + ch) * audio_t + i];
665 }
666 }
667 }
668 out
669 };
670 for (ci_, cur) in chunks.iter().enumerate() {
671 let by_chunk = std::env::var("CMF_STREAM_UNIT").as_deref() == Ok("chunks");
680 let ctx_v: Vec<usize> = if by_chunk {
681 let mut vis: Vec<usize> = (0..p.stream_sink.min(ci_)).collect();
682 for j in ci_.saturating_sub(p.stream_window)..ci_ {
683 if !vis.contains(&j) {
684 vis.push(j);
685 }
686 }
687 vis.sort_unstable();
688 vis.iter().flat_map(|&j| chunks[j].clone()).collect()
689 } else {
690 let done = cur.start; let mut f: Vec<usize> = (0..p.stream_sink.min(done)).collect();
692 for k in done.saturating_sub(p.stream_window)..done {
693 if !f.contains(&k) {
694 f.push(k);
695 }
696 }
697 f.sort_unstable();
698 f
699 };
700 let cur_v: Vec<usize> = cur.clone().collect();
701 let mut ctx_a: Vec<usize> = Vec::new();
703 for &k in &ctx_v {
704 for i in a_bound(k)..a_bound(k + 1) {
705 if !ctx_a.contains(&i) {
706 ctx_a.push(i);
707 }
708 }
709 }
710 ctx_a.sort_unstable();
711 let cur_a: Vec<usize> = (a_bound(cur.start)..a_bound(cur.end)).collect();
712 let clay = Layout::streaming(
713 ids.len(),
714 &tags,
715 lat_h,
716 lat_w,
717 &ctx_v,
718 &cur_v,
719 &ctx_a,
720 &cur_a,
721 );
722 let ctx_vx = gather_v(&v_out, &ctx_v);
723 let ctx_ax = gather_a(&a_out, &ctx_a);
724 let mut xv = gather_v(&v, &cur_v);
725 let mut xa = gather_a(&a, &cur_a);
726 for i in 0..p.steps {
727 let (sv, sv_n) = (sg[i], sg[i + 1]);
728 let mut vin = vec![0f32; vc * (ctx_v.len() + cur_v.len()) * vframe];
731 let mut ain = vec![0f32; ac * 2 * (ctx_a.len() + cur_a.len())];
732 let (nc, nk) = (ctx_v.len(), cur_v.len());
733 for c in 0..vc {
734 let d = c * (nc + nk) * vframe;
735 vin[d..d + nc * vframe]
736 .copy_from_slice(&ctx_vx[c * nc * vframe..(c + 1) * nc * vframe]);
737 vin[d + nc * vframe..d + (nc + nk) * vframe]
738 .copy_from_slice(&xv[c * nk * vframe..(c + 1) * nk * vframe]);
739 }
740 let (mc, mk) = (ctx_a.len(), cur_a.len());
741 for c in 0..ac {
742 for ch in 0..2 {
743 let d = (c * 2 + ch) * (mc + mk);
744 ain[d..d + mc]
745 .copy_from_slice(&ctx_ax[(c * 2 + ch) * mc..(c * 2 + ch + 1) * mc]);
746 ain[d + mc..d + mc + mk]
747 .copy_from_slice(&xa[(c * 2 + ch) * mk..(c * 2 + ch + 1) * mk]);
748 }
749 }
750 let (dv, da) = dit.forward(&clay, &text, &vin, &ain, sv, &[]);
751 let step_v = (sv_n - sv) as f32;
752 for (x, &d) in xv.iter_mut().zip(&dv) {
753 *x += step_v * d;
754 }
755 let step_a = if p.stock_sampler {
756 step_v
757 } else {
758 (time_shift_sigma(sv_n, dit.shift_video, dit.shift_audio)
759 - time_shift_sigma(sv.max(1e-6), dit.shift_video, dit.shift_audio))
760 as f32
761 };
762 for (x, &d) in xa.iter_mut().zip(&da) {
763 *x += step_a * d;
764 }
765 }
766 for c in 0..vc {
767 for (n, &k) in cur_v.iter().enumerate() {
768 let d = (c * latent_t + k) * vframe;
769 let s = (c * cur_v.len() + n) * vframe;
770 v_out[d..d + vframe].copy_from_slice(&xv[s..s + vframe]);
771 }
772 }
773 for c in 0..ac {
774 for ch in 0..2 {
775 for (n, &i) in cur_a.iter().enumerate() {
776 a_out[(c * 2 + ch) * audio_t + i] = xa[(c * 2 + ch) * cur_a.len() + n];
777 }
778 }
779 }
780 progress("stream", ci_ + 1, chunks.len());
781 }
782 if let Some(rep) = dit.lora_report() {
783 eprint!("{rep}");
784 }
785 return_streaming = Some((v_out, a_out));
786 }
787
788 for i in 0..p.steps {
789 if return_streaming.is_some() {
790 break;
791 }
792 let (sv, sv_n) = (sg[i], sg[i + 1]);
793 let (dv, da) = dit.forward(&layout, &text, &v, &a, sv, &cond);
794 if let Ok(pre) = std::env::var("CMF_ANIM_DUMP") {
799 let raw = |x: &[f32]| x.iter().flat_map(|v| v.to_le_bytes()).collect::<Vec<u8>>();
800 let _ = std::fs::write(format!("{pre}.v{i}.f32"), raw(&dv));
801 let _ = std::fs::write(format!("{pre}.a{i}.f32"), raw(&da));
802 }
803 let step_v = (sv_n - sv) as f32;
804 for (x, &d) in v.iter_mut().zip(&dv) {
805 *x += step_v * d;
806 }
807 let step_a = if p.stock_sampler {
808 step_v
809 } else {
810 (time_shift_sigma(sv_n, dit.shift_video, dit.shift_audio)
811 - time_shift_sigma(sv.max(1e-6), dit.shift_video, dit.shift_audio))
812 as f32
813 };
814 for (x, &d) in a.iter_mut().zip(&da) {
815 *x += step_a * d;
816 }
817 if prof {
818 eprintln!(
819 " step {i}: sv {sv:.4}->{sv_n:.4} v_vel {:.4} a_vel {:.4} | video {:.4} audio {:.4}",
820 rms(&dv),
821 rms(&da),
822 rms(&v),
823 rms(&a)
824 );
825 }
826 progress("denoise", i + 1, p.steps);
827 }
828 if let Some(rep) = dit.lora_report() {
829 eprint!("{rep}");
830 }
831 match return_streaming {
832 Some(pair) => pair,
833 None => (v, a),
834 }
835 };
836
837 lap(&mut marks, "denoise");
838 crate::gpu::stage_done(&model);
840
841 let (mut out_h, mut out_w) = (p.height, p.width);
843 let (mut lat_h, mut lat_w) = (lat_h, lat_w);
844 if let Some(path) = p.upscale.as_deref() {
845 progress("upscale", 0, 1);
846 let t = std::time::Instant::now();
847 let ups = crate::mmh3ups::LatentUpscaler::load(std::path::Path::new(path))?;
848 let z = crate::mmh3ups::Vol {
849 c: video.len() / (latent_t * lat_h * lat_w),
850 t: latent_t,
851 h: lat_h,
852 w: lat_w,
853 data: video,
854 };
855 let f = p.upscale_by.max(1.0);
858 let nh = ((lat_h as f32 * f).round() as usize).max(lat_h);
859 let nw = ((lat_w as f32 * f).round() as usize).max(lat_w);
860 let big = ups.upscale(&z, nh, nw, None);
861 tracing::info!(
862 "latent upscale {}x{} -> {}x{} in {:.1}s",
863 lat_h,
864 lat_w,
865 nh,
866 nw,
867 t.elapsed().as_secs_f64()
868 );
869 out_h = out_h * nh / lat_h;
870 out_w = out_w * nw / lat_w;
871 lat_h = nh;
872 lat_w = nw;
873 video = big.data;
874 progress("upscale", 1, 1);
875 lap(&mut marks, "upscale");
876 }
877
878 progress("video vae", 0, 1);
880 let (rgb, out_frames) = {
881 let vae = VideoVae::from_cmf(&model)?;
882 vae.decode(&video, latent_t, lat_h, lat_w)
883 };
884 progress("video vae", 1, 1);
885 lap(&mut marks, "video vae");
886 crate::gpu::stage_done(&model);
887 progress("audio vae", 0, 1);
888 let (wave, samples, sr) = {
889 let vae = AudioVae::from_cmf(&model)?;
890 let c = audio.len() / (2 * audio_t);
891 let (w, n) = vae.decode(&audio, c, audio_t);
892 (w, n, vae.sample_rate)
893 };
894 progress("audio vae", 1, 1);
895 lap(&mut marks, "audio vae");
896 tracing::info!(
897 "stages: {}",
898 marks
899 .iter()
900 .map(|(n, v)| format!("{n} {v:.1}s"))
901 .collect::<Vec<_>>()
902 .join(" · ")
903 );
904 #[cfg(target_os = "macos")]
908 if std::env::var("CMF_METAL_MMPROF").is_ok() {
909 use std::sync::atomic::Ordering::Relaxed;
910 let n = crate::gpu_metal::MM_N.load(Relaxed);
911 eprintln!(
912 " q4tp mm x{n}: upload {:.1}s · submit+wait {:.1}s · readback {:.1}s",
913 crate::gpu_metal::MM_UP.load(Relaxed) as f64 / 1e6,
914 crate::gpu_metal::MM_GPU.load(Relaxed) as f64 / 1e6,
915 crate::gpu_metal::MM_DN.load(Relaxed) as f64 / 1e6,
916 );
917 }
918
919 let keep = out_frames.min(frames_total);
923 Ok(Anim {
924 rgb: trim_frames(&rgb, out_frames, keep, out_h, out_w),
925 frames: keep,
926 height: out_h,
927 width: out_w,
928 audio: wave,
929 samples,
930 sample_rate: sr,
931 })
932}
933
934fn trim_frames(rgb: &[f32], have: usize, keep: usize, h: usize, w: usize) -> Vec<f32> {
935 if keep == have {
936 return rgb.to_vec();
937 }
938 let mut out = vec![0f32; 3 * keep * h * w];
939 for c in 0..3 {
940 let s = c * have * h * w;
941 let d = c * keep * h * w;
942 out[d..d + keep * h * w].copy_from_slice(&rgb[s..s + keep * h * w]);
943 }
944 out
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 #[test]
952 fn the_four_step_schedule_is_the_references() {
953 let s = sigmas(4, 12.0);
954 let want = [1.0, 0.972_973, 0.923_077, 0.8, 0.0];
955 assert_eq!(s.len(), want.len());
956 for (g, w) in s.iter().zip(&want) {
957 assert!((g - w).abs() < 1e-6, "{s:?}");
958 }
959 }
960
961 #[test]
962 fn a_stretch_keeps_the_corners_and_a_crop_takes_the_middle() {
963 let (h, w) = (2usize, 4usize);
966 let mut rgb = vec![0f32; 3 * h * w];
967 for c in 0..3 {
968 for y in 0..h {
969 for x in 0..w {
970 rgb[(c * h + y) * w + x] = x as f32 / (w - 1) as f32;
971 }
972 }
973 }
974 let s = fit_to_canvas(&rgb, h, w, 4, 4, false);
976 assert!((s[0] - 0.0).abs() < 1e-6, "left edge");
977 assert!((s[3] - 1.0).abs() < 1e-6, "right edge");
978 let c = fit_to_canvas(&rgb, h, w, 4, 4, true);
981 let (lo, hi) = c[..16]
982 .iter()
983 .fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
984 assert!(lo > 0.05, "crop kept the left edge: {lo}");
985 assert!(hi < 0.95, "crop kept the right edge: {hi}");
986 }
987
988 #[test]
989 fn frame_counts_snap_to_the_models_grid() {
990 assert_eq!(align_frames(1), 5);
992 assert_eq!(align_frames(39), 39);
993 assert_eq!(align_frames(41), 56);
994 assert_eq!(align_frames(124), 124);
995 assert_eq!(video_latent_t(124), 37);
996 assert_eq!(video_latent_t(39), 12);
997 let (f, lt, at) = temporal_shape(124);
998 assert_eq!((f, lt, at), (124, 37, 207));
999 }
1000}