1use crate::audiovae::AudioVae;
20use crate::mmh3::{Layout, MiniMaxH3, time_shift_sigma};
21use crate::qwen3te::{ImageSpan, Qwen3Encoder};
22use crate::qwen3vis::{self, VisionTower};
23use crate::vae3d::VideoVaeEncoder;
24use crate::sampler::SplitMix64;
25use crate::tokenizer::Tokenizer;
26use crate::vae3d::VideoVae;
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}
49
50const VISION_START: u32 = 151_652;
52const VISION_END: u32 = 151_653;
53
54pub fn fit_to_canvas(
58 rgb: &[f32],
59 h: usize,
60 w: usize,
61 out_h: usize,
62 out_w: usize,
63 crop: bool,
64) -> Vec<f32> {
65 let (sx0, sy0, sw, sh) = if crop {
68 let (tw, th) = (out_w as f64, out_h as f64);
69 let scale = (w as f64 / tw).min(h as f64 / th);
70 let (cw, ch) = ((tw * scale).round() as usize, (th * scale).round() as usize);
71 ((w - cw) / 2, (h - ch) / 2, cw.max(1), ch.max(1))
72 } else {
73 (0, 0, w, h)
74 };
75 let mut out = vec![0f32; 3 * out_h * out_w];
76 for c in 0..3 {
77 for y in 0..out_h {
78 let sy = ((y as f64 + 0.5) * sh as f64 / out_h as f64 - 0.5).max(0.0);
79 let y0 = sy.floor() as usize;
80 let y1 = (y0 + 1).min(sh - 1);
81 let fy = (sy - y0 as f64) as f32;
82 for x in 0..out_w {
83 let sx = ((x as f64 + 0.5) * sw as f64 / out_w as f64 - 0.5).max(0.0);
84 let x0 = sx.floor() as usize;
85 let x1 = (x0 + 1).min(sw - 1);
86 let fx = (sx - x0 as f64) as f32;
87 let p = |yy: usize, xx: usize| rgb[(c * h + sy0 + yy) * w + sx0 + xx];
88 let top = p(y0, x0) * (1.0 - fx) + p(y0, x1) * fx;
89 let bot = p(y1, x0) * (1.0 - fx) + p(y1, x1) * fx;
90 out[(c * out_h + y) * out_w + x] = top * (1.0 - fy) + bot * fy;
91 }
92 }
93 }
94 out
95}
96
97impl Default for AnimParams {
98 fn default() -> Self {
99 Self {
100 width: 512,
101 height: 288,
102 frames: 39,
103 steps: 4,
104 seed: 42,
105 stock_sampler: false,
106 max_tokens: 512,
107 first_frame: None,
108 last_frame: None,
109 }
110 }
111}
112
113pub struct Anim {
116 pub rgb: Vec<f32>,
117 pub frames: usize,
118 pub height: usize,
119 pub width: usize,
120 pub audio: Vec<f32>,
121 pub samples: usize,
122 pub sample_rate: usize,
123}
124
125pub fn align_frames(n: usize) -> usize {
128 let mut n = n.max(5);
129 while n % 17 != 5 {
130 n += 1;
131 }
132 n
133}
134
135pub fn video_latent_t(frames: usize) -> usize {
136 if frames <= 5 {
137 2
138 } else {
139 (frames - 5) / 17 * 5 + 2
140 }
141}
142
143pub fn temporal_shape(len: usize) -> (usize, usize, usize) {
145 let frames = align_frames(len);
146 let audio_t =
147 ((frames as f64 / FPS as f64) * AUDIO_LATENT_FPS as f64).round() as usize;
148 (frames, video_latent_t(frames), audio_t)
149}
150
151pub fn sigmas(steps: usize, shift: f64) -> Vec<f64> {
154 let table = 1000usize;
155 let mut out: Vec<f64> = (0..steps)
156 .map(|x| {
157 let idx = table - 1 - x * table / steps;
158 let t = (idx + 1) as f64 / table as f64;
159 shift * t / (1.0 + (shift - 1.0) * t)
160 })
161 .collect();
162 out.push(0.0);
163 out
164}
165
166pub fn gauss_pub(n: usize, seed: u64) -> Vec<f32> {
168 gauss(n, seed)
169}
170
171fn gauss(n: usize, seed: u64) -> Vec<f32> {
172 let mut rng = SplitMix64::new(seed);
173 let mut u = || (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
174 let mut out = Vec::with_capacity(n);
175 while out.len() < n {
176 let (a, b) = (u().max(1e-300), u());
177 let r = (-2.0 * a.ln()).sqrt();
178 let ang = 2.0 * std::f64::consts::PI * b;
179 out.push((r * ang.cos()) as f32);
180 if out.len() < n {
181 out.push((r * ang.sin()) as f32);
182 }
183 }
184 out
185}
186
187pub fn generate(
189 path: &Path,
190 prompt: &str,
191 p: &AnimParams,
192 mut progress: impl FnMut(&str, usize, usize),
193) -> Result<Anim, String> {
194 if p.width % 32 != 0 || p.height % 32 != 0 {
195 return Err("width/height must be multiples of 32".into());
196 }
197 let use_gpu = match std::env::var("CMF_MMH3_GPU").ok().as_deref() {
205 Some("1") => true,
206 Some("0") => false,
207 _ => mmh3_gpu_parity_probe(path).unwrap_or(false),
208 };
209 if use_gpu {
210 generate_inner(path, prompt, p, &mut progress)
211 } else {
212 crate::gpu::cpu_scope(|| generate_inner(path, prompt, p, &mut progress))
213 }
214}
215
216fn mmh3_gpu_parity_probe(path: &Path) -> Result<bool, String> {
223 let model = Arc::new(
224 cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
225 );
226 let Some(idx) = model.tensors.iter().position(|t| {
227 t.name.starts_with("dit.")
228 && t.name.ends_with("attn.qkv_proj.weight")
229 && t.dtype == cortiq_core::TensorDtype::Q4TiledP
230 }) else {
231 tracing::info!("mmh3 GPU parity probe: no q4tp qkv tensor — host path");
232 return Ok(false);
233 };
234 let entry = &model.tensors[idx];
235 let (rows, cols) = (entry.shape[0], entry.shape[1]);
236 let b = 64usize;
237 let mut xs = vec![0f32; b * cols];
238 for (i, v) in xs.iter_mut().enumerate() {
239 let base = ((i * 37 + 11) % 1009) as f32 / 1009.0 - 0.5;
240 *v = base * if i % 7 == 0 { 2000.0 } else { 2.0 };
241 }
242 let mut gpu = vec![0f32; b * rows];
243 if std::env::var("CMF_GPU_DEBUG").is_ok() {
244 eprintln!(
245 "mmh3 probe env: CMF_GPU={:?} enabled={} avail={}",
246 std::env::var("CMF_GPU").ok(),
247 crate::gpu::enabled(),
248 crate::gpu::backend_available(),
249 );
250 }
251 if !crate::gpu::q4tp_matmat(&model, idx, &xs, b, rows, cols, &mut gpu) {
252 tracing::info!(
253 "mmh3 GPU parity probe: q4tp_matmat refused ({rows}x{cols}) — host path"
254 );
255 if std::env::var("CMF_GPU_DEBUG").is_ok() {
256 eprintln!("mmh3 probe: q4tp_matmat refused {rows}x{cols}");
257 }
258 return Ok(false);
259 }
260 let host = {
261 let name = entry.name.clone();
262 let proj = crate::dit::Proj::from_model(&model, &name)?;
263 let mut out = vec![0f32; b * rows];
264 crate::gpu::cpu_scope(|| proj.matmat(&xs, b, &mut out, None));
265 out
266 };
267 let mut num = 0f64;
268 let mut den = 0f64;
269 for (g, h) in gpu.iter().zip(&host) {
270 num += ((g - h) as f64).powi(2);
271 den += (*h as f64).powi(2);
272 }
273 let rel = (num / den.max(1e-30)).sqrt();
274 let ok = rel < 1e-2;
275 tracing::info!(
276 "mmh3 GPU parity probe: rel rms {rel:.2e} → {}",
277 if ok { "device" } else { "host" }
278 );
279 if std::env::var("CMF_GPU_DEBUG").is_ok() {
280 eprintln!("mmh3 GPU parity probe: rel rms {rel:.2e}");
281 }
282 Ok(ok)
283}
284
285fn generate_inner(
286 path: &Path,
287 prompt: &str,
288 p: &AnimParams,
289 progress: &mut dyn FnMut(&str, usize, usize),
290) -> Result<Anim, String> {
291 let model = Arc::new(
292 cortiq_core::CmfModel::open(path).map_err(|e| format!("{}: {e}", path.display()))?,
293 );
294 let (frames_total, latent_t, audio_t) = temporal_shape(p.frames);
295 let (lat_h, lat_w) = (p.height / 16, p.width / 16);
296
297 let t_stage = std::time::Instant::now();
304 let mut marks: Vec<(&str, f32)> = Vec::new();
305 let mut lap = |marks: &mut Vec<(&'static str, f32)>, name: &'static str| {
306 let prev: f32 = marks.iter().map(|(_, v)| v).sum();
307 marks.push((name, t_stage.elapsed().as_secs_f32() - prev));
308 };
309 let vocab = model
310 .vocab
311 .as_deref()
312 .ok_or("packaged .cmf has no embedded tokenizer")?;
313 let tok = Tokenizer::from_bytes(vocab).map_err(|e| format!("tokenizer: {e}"))?;
314 let keyframes: Vec<(&(Vec<f32>, usize, usize), usize)> = p
318 .first_frame
319 .iter()
320 .map(|f| (f, 0usize))
321 .chain(p.last_frame.iter().map(|f| (f, frames_total - 1)))
322 .collect();
323 let mut ids: Vec<u32> = Vec::new();
324 let mut spans: Vec<ImageSpan> = Vec::new();
325 let mut embeds: Vec<Vec<f32>> = Vec::new();
326 let mut deepstack: Vec<Vec<f32>> = Vec::new();
327 let mut cond: Vec<Vec<f32>> = Vec::new();
328 let mut tags: Vec<u8> = Vec::new();
329
330 if !keyframes.is_empty() {
331 let tower = VisionTower::from_cmf(&model)?;
332 let venc = VideoVaeEncoder::from_cmf(&model)?;
333 for (i, (frame, _)) in keyframes.iter().enumerate() {
334 let (src, sh, sw) = *frame;
335 let fitted = fit_to_canvas(src, *sh, *sw, p.height, p.width, i > 0);
338 let (z, _, _) = venc.encode_frame(
339 &fitted.iter().map(|&v| v * 2.0 - 1.0).collect::<Vec<_>>(),
340 p.height,
341 p.width,
342 );
343 cond.push(z);
344
345 for t in tok.encode(&format!("<Picture {}>: ", i + 1)) {
346 ids.push(t);
347 tags.push(1);
348 }
349 let (patches, gh, gw) = qwen3vis::preprocess(
350 &fitted, p.height, p.width,
351 tower.patch_size, tower.temporal_patch, tower.merge,
352 );
353 let (merged, deep) = tower.forward(&patches, gh, gw);
354 let n_img = merged.len() / tower.out_hidden;
355 ids.push(VISION_START);
358 tags.push(0);
359 let start = ids.len();
360 for _ in 0..n_img {
361 ids.push(VISION_START); tags.push(0);
363 }
364 ids.push(VISION_END);
365 tags.push(0);
366 spans.push(ImageSpan { start, len: n_img, merged_h: gh / tower.merge, merged_w: gw / tower.merge });
367 embeds.push(merged);
368 if deepstack.is_empty() {
369 deepstack = deep;
370 } else {
371 for (a, b) in deepstack.iter_mut().zip(deep) {
372 a.extend_from_slice(&b);
373 }
374 }
375 }
376 }
377 for t in tok.encode(prompt) {
378 ids.push(t);
379 tags.push(1);
380 }
381 if ids.is_empty() {
382 ids.push(151643); tags.push(1);
384 }
385 ids.truncate(p.max_tokens);
386 tags.truncate(ids.len());
387 lap(&mut marks, "prepare");
388 progress("encode", 0, 1);
389 let states = {
390 let enc = Qwen3Encoder::from_cmf(&model)?;
391 enc.encode_with_images(&ids, &spans, &embeds, &deepstack)
392 };
393 progress("encode", 1, 1);
394 lap(&mut marks, "text encode");
395
396 let (video, audio) = {
398 let dit = MiniMaxH3::from_cmf(&model)?;
399 let kf: Vec<(usize, usize)> = keyframes.iter().map(|&(_, idx)| (idx, frames_total)).collect();
400 let layout = if kf.is_empty() {
401 Layout::t2va(ids.len(), latent_t, lat_h, lat_w, audio_t)
402 } else {
403 Layout::fl2va(ids.len(), latent_t, lat_h, lat_w, audio_t, &kf, &tags)
404 };
405 let text = dit.refine_text(&states, ids.len());
406 let mut v = gauss(dit.latents_dim * latent_t * lat_h * lat_w, p.seed);
407 let mut a = gauss(dit.audio_dim * 2 * audio_t, p.seed ^ 0x9E37_79B9_7F4A_7C15);
408 let sg = sigmas(p.steps, dit.shift_video);
409 let prof = std::env::var_os("CMF_ANIM_PROF").is_some();
413 let rms = |x: &[f32]| {
414 (x.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / x.len() as f64).sqrt()
415 };
416 if prof {
417 eprintln!(
418 " text {} tok, refined rms {:.4}, sigmas {:?}",
419 ids.len(),
420 rms(&text),
421 sg.iter().map(|v| (v * 1e4).round() / 1e4).collect::<Vec<_>>()
422 );
423 }
424 for i in 0..p.steps {
425 let (sv, sv_n) = (sg[i], sg[i + 1]);
426 let (dv, da) = dit.forward(&layout, &text, &v, &a, sv, &cond);
427 let step_v = (sv_n - sv) as f32;
428 for (x, &d) in v.iter_mut().zip(&dv) {
429 *x += step_v * d;
430 }
431 let step_a = if p.stock_sampler {
432 step_v
433 } else {
434 (time_shift_sigma(sv_n, dit.shift_video, dit.shift_audio)
435 - time_shift_sigma(sv.max(1e-6), dit.shift_video, dit.shift_audio))
436 as f32
437 };
438 for (x, &d) in a.iter_mut().zip(&da) {
439 *x += step_a * d;
440 }
441 if prof {
442 eprintln!(
443 " step {i}: sv {sv:.4}->{sv_n:.4} v_vel {:.4} a_vel {:.4} | video {:.4} audio {:.4}",
444 rms(&dv), rms(&da), rms(&v), rms(&a)
445 );
446 }
447 progress("denoise", i + 1, p.steps);
448 }
449 (v, a)
450 };
451
452 lap(&mut marks, "denoise");
453 progress("video vae", 0, 1);
455 let (rgb, out_frames) = {
456 let vae = VideoVae::from_cmf(&model)?;
457 vae.decode(&video, latent_t, lat_h, lat_w)
458 };
459 progress("video vae", 1, 1);
460 lap(&mut marks, "video vae");
461 progress("audio vae", 0, 1);
462 let (wave, samples, sr) = {
463 let vae = AudioVae::from_cmf(&model)?;
464 let c = audio.len() / (2 * audio_t);
465 let (w, n) = vae.decode(&audio, c, audio_t);
466 (w, n, vae.sample_rate)
467 };
468 progress("audio vae", 1, 1);
469 lap(&mut marks, "audio vae");
470 tracing::info!(
471 "stages: {}",
472 marks
473 .iter()
474 .map(|(n, v)| format!("{n} {v:.1}s"))
475 .collect::<Vec<_>>()
476 .join(" · ")
477 );
478
479 let keep = out_frames.min(frames_total);
483 Ok(Anim {
484 rgb: trim_frames(&rgb, out_frames, keep, p.height, p.width),
485 frames: keep,
486 height: p.height,
487 width: p.width,
488 audio: wave,
489 samples,
490 sample_rate: sr,
491 })
492}
493
494fn trim_frames(rgb: &[f32], have: usize, keep: usize, h: usize, w: usize) -> Vec<f32> {
495 if keep == have {
496 return rgb.to_vec();
497 }
498 let mut out = vec![0f32; 3 * keep * h * w];
499 for c in 0..3 {
500 let s = c * have * h * w;
501 let d = c * keep * h * w;
502 out[d..d + keep * h * w].copy_from_slice(&rgb[s..s + keep * h * w]);
503 }
504 out
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn the_four_step_schedule_is_the_references() {
513 let s = sigmas(4, 12.0);
514 let want = [1.0, 0.972_973, 0.923_077, 0.8, 0.0];
515 assert_eq!(s.len(), want.len());
516 for (g, w) in s.iter().zip(&want) {
517 assert!((g - w).abs() < 1e-6, "{s:?}");
518 }
519 }
520
521 #[test]
522 fn a_stretch_keeps_the_corners_and_a_crop_takes_the_middle() {
523 let (h, w) = (2usize, 4usize);
526 let mut rgb = vec![0f32; 3 * h * w];
527 for c in 0..3 {
528 for y in 0..h {
529 for x in 0..w {
530 rgb[(c * h + y) * w + x] = x as f32 / (w - 1) as f32;
531 }
532 }
533 }
534 let s = fit_to_canvas(&rgb, h, w, 4, 4, false);
536 assert!((s[0] - 0.0).abs() < 1e-6, "left edge");
537 assert!((s[3] - 1.0).abs() < 1e-6, "right edge");
538 let c = fit_to_canvas(&rgb, h, w, 4, 4, true);
541 let (lo, hi) = c[..16]
542 .iter()
543 .fold((f32::MAX, f32::MIN), |(a, b), &v| (a.min(v), b.max(v)));
544 assert!(lo > 0.05, "crop kept the left edge: {lo}");
545 assert!(hi < 0.95, "crop kept the right edge: {hi}");
546 }
547
548 #[test]
549 fn frame_counts_snap_to_the_models_grid() {
550 assert_eq!(align_frames(1), 5);
552 assert_eq!(align_frames(39), 39);
553 assert_eq!(align_frames(41), 56);
554 assert_eq!(align_frames(124), 124);
555 assert_eq!(video_latent_t(124), 37);
556 assert_eq!(video_latent_t(39), 12);
557 let (f, lt, at) = temporal_shape(124);
558 assert_eq!((f, lt, at), (124, 37, 207));
559 }
560}