use anyhow::{Context, anyhow};
use cortiq_core::CmfModel;
use cortiq_engine::audiovae::Music3Dav;
use cortiq_engine::music3::{Music3Ar, Music3Dit, tokens};
use cortiq_engine::tokenizer::Tokenizer;
use std::sync::Arc;
fn normalize_lyrics(lyrics: &str) -> String {
let mut out = String::from("[start]\n");
for line in lyrics.replace(" ^ ", "\n").lines() {
let l = line.trim();
if l.is_empty() {
continue;
}
if l.starts_with('[') && l.ends_with(']') {
out.push_str(&l.to_lowercase());
} else {
out.push_str(l);
}
out.push('\n');
}
out
}
fn build_ids(tok: &Tokenizer, caption: &str, lyrics: &str) -> Vec<u32> {
let mut ids = vec![tokens::IM_START, tokens::CAPTION_START];
ids.extend(tok.encode(caption.trim()));
ids.push(tokens::CAPTION_END);
ids.push(tokens::LYRICS_START);
ids.extend(tok.encode(&normalize_lyrics(lyrics)));
ids.push(tokens::LYRICS_END);
ids.push(tokens::IM_END);
ids.push(tokens::AUDIO_START);
ids
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_music(
model_path: &str,
caption: &str,
lyrics: &str,
seconds: f32,
steps: usize,
seed: u64,
out: &str,
) -> anyhow::Result<()> {
let t0 = std::time::Instant::now();
let model = Arc::new(CmfModel::open(model_path).map_err(|e| anyhow!("{model_path}: {e}"))?);
let vocab = model
.vocab
.as_deref()
.ok_or_else(|| anyhow!("{model_path} carries no tokenizer"))?;
let tok = Tokenizer::from_bytes(vocab).map_err(|e| anyhow!("tokenizer: {e}"))?;
let ar = Music3Ar::from_cmf(&model).map_err(|e| anyhow!("AR stack: {e}"))?;
let ids = build_ids(&tok, caption, lyrics);
let frames = ((seconds * ar.fps as f32).round() as usize)
.clamp(1, ar.max_frames)
.min(ar.max_frames);
eprintln!(
"prompt {} tokens, asking for {frames} audio frames ({:.1} s at {} fps)",
ids.len(),
frames as f32 / ar.fps as f32,
ar.fps
);
let (hidden, got) = ar
.generate(&ids, seed, frames, |i, n| {
if i > 0 && (i % 10 == 0 || i == n) {
eprint!("\r ar {i}/{n} ({:.1}s) ", t0.elapsed().as_secs_f32());
}
})
.map_err(|e| anyhow!("{e}"))?;
eprintln!("\n ar done: {got} frames ({:.1}s)", t0.elapsed().as_secs_f32());
let dit = Music3Dit::from_cmf(&model).map_err(|e| anyhow!("DiT: {e}"))?;
let (cond, n) = dit.aligned_condition(&hidden, got);
eprintln!(" condition {} x {n} latent frames", Music3Dit::COND_CH);
let mut rng = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
let noise: Vec<f32> = (0..Music3Dit::IN_CH * n)
.map(|_| {
rng ^= rng << 13;
rng ^= rng >> 7;
rng ^= rng << 17;
let u = |x: u64| ((x >> 40) as f32) / (1u32 << 24) as f32 - 0.5;
(u(rng) + u(rng.rotate_left(17)) + u(rng.rotate_left(34))) * 2.0
})
.collect();
let latent = dit.sample(&noise, &cond, n, steps, |i, t| {
eprint!("\r denoise {i}/{t} ({:.1}s) ", t0.elapsed().as_secs_f32());
});
eprintln!();
let dav = Music3Dav::from_cmf(&model).map_err(|e| anyhow!("vocoder: {e}"))?;
let pool = cortiq_engine::pool::Pool::from_env();
let pcm = dav.decode(&latent, n, pool.as_deref());
let samples = pcm.len() / 2;
let mut planar = vec![0f32; pcm.len()];
for (i, c) in pcm.chunks_exact(2).enumerate() {
planar[i] = c[0];
planar[samples + i] = c[1];
}
let wav = crate::avout::wav_bytes(&planar, samples, Music3Dav::SAMPLE_RATE);
std::fs::write(out, &wav).with_context(|| out.to_string())?;
let secs = samples as f32 / Music3Dav::SAMPLE_RATE as f32;
println!(
"{out}: {secs:.2}s stereo at {} Hz, {:.1} MB in {:.1}s",
Music3Dav::SAMPLE_RATE,
wav.len() as f32 / 1e6,
t0.elapsed().as_secs_f32()
);
Ok(())
}