1use std::collections::HashMap;
24use std::path::Path;
25
26use anyhow::{Context, Result};
27use burn::prelude::Backend;
28use burn::tensor::{DType, Int, Tensor, TensorData};
29use burn_store::ModuleStore;
30use rand::rngs::SmallRng;
31use rand::{RngExt, SeedableRng};
32
33use super::qwen3::{Qwen3Config, Qwen3Model};
34
35pub const IM_END_ID: u32 = 151_645;
37pub const ENDOFTEXT_ID: u32 = 151_643;
39pub const MAX_AUDIO_CODE: u32 = 63_999;
41pub const AUDIO_CODES_PER_SECOND: usize = 5;
43pub const DEFAULT_TEMPERATURE: f32 = 0.85;
45
46pub fn code_count_for_duration(duration_s: usize) -> usize {
48 duration_s * AUDIO_CODES_PER_SECOND
49}
50
51pub fn build_cot_block(
56 caption: &str,
57 bpm: Option<f32>,
58 key_scale: Option<&str>,
59 time_signature: Option<&str>,
60 duration_s: usize,
61) -> String {
62 let mut lines = Vec::new();
63 if let Some(bpm) = bpm {
64 if bpm.fract() == 0.0 {
65 lines.push(format!("bpm: {}", bpm as i64));
66 } else {
67 lines.push(format!("bpm: {bpm}"));
68 }
69 }
70 lines.push(format!("caption: {caption}"));
71 lines.push(format!("duration: {duration_s}"));
72 if let Some(key_scale) = key_scale {
73 lines.push(format!("keyscale: {key_scale}"));
74 }
75 lines.push("language: unknown".to_string());
76 if let Some(time_signature) = time_signature {
77 lines.push(format!("timesignature: {time_signature}"));
78 }
79 format!("<think>\n{}\n</think>", lines.join("\n"))
80}
81
82pub fn build_uncond_codes_prompt() -> String {
86 "<|im_start|>system\n\
87 # Instruction\n\
88 Generate audio semantic tokens based on the given conditions:\n\
89 \n\
90 <|im_end|>\n\
91 <|im_start|>user\n\
92 <|im_end|>\n\
93 <|im_start|>assistant\n\
94 <think>\n\
95 \n\
96 </think>\n\
97 \n"
98 .to_string()
99}
100
101pub fn build_codes_prompt(caption: &str, cot_block: &str) -> String {
106 format!(
107 "<|im_start|>system\n\
108 # Instruction\n\
109 Generate audio semantic tokens based on the given conditions:\n\
110 \n\
111 <|im_end|>\n\
112 <|im_start|>user\n\
113 # Caption\n\
114 {caption}\n\
115 \n\
116 # Lyric\n\
117 [Instrumental]\n\
118 <|im_end|>\n\
119 <|im_start|>assistant\n\
120 {cot_block}\n\
121 \n"
122 )
123}
124
125#[derive(Clone, Debug, Default)]
131pub struct AudioCodeVocab {
132 code_to_token: HashMap<u32, u32>,
133 token_to_code: HashMap<u32, u32>,
134}
135
136impl AudioCodeVocab {
137 pub const IM_END_ID: u32 = IM_END_ID;
139 pub const ENDOFTEXT_ID: u32 = ENDOFTEXT_ID;
141
142 pub fn from_tokenizer_json(path: &Path) -> Result<Self> {
143 let text = std::fs::read_to_string(path)
144 .with_context(|| format!("failed to read tokenizer from {}", path.display()))?;
145 let data: serde_json::Value = serde_json::from_str(&text)
146 .with_context(|| format!("failed to parse tokenizer from {}", path.display()))?;
147 let mut vocab = Self::default();
148 if let Some(added) = data.get("added_tokens").and_then(|v| v.as_array()) {
149 for entry in added {
150 let Some(content) = entry.get("content").and_then(|c| c.as_str()) else {
151 continue;
152 };
153 let Some(id) = entry.get("id").and_then(|i| i.as_u64()) else {
154 continue;
155 };
156 let Some(code) = parse_audio_code_content(content) else {
157 continue;
158 };
159 let id = u32::try_from(id)
160 .with_context(|| format!("token id out of range for {content}"))?;
161 vocab.code_to_token.insert(code, id);
162 vocab.token_to_code.insert(id, code);
163 }
164 }
165 Ok(vocab)
166 }
167
168 pub fn code_token_id(&self, n: u32) -> Option<u32> {
170 self.code_to_token.get(&n).copied()
171 }
172
173 pub fn token_id_to_code(&self, id: u32) -> Option<u32> {
176 self.token_to_code.get(&id).copied()
177 }
178
179 pub fn len(&self) -> usize {
180 self.code_to_token.len()
181 }
182
183 pub fn is_empty(&self) -> bool {
184 self.code_to_token.is_empty()
185 }
186}
187
188fn parse_audio_code_content(content: &str) -> Option<u32> {
189 content
190 .strip_prefix("<|audio_code_")?
191 .strip_suffix("|>")?
192 .parse()
193 .ok()
194}
195
196#[derive(Clone, Copy, Debug)]
198pub struct SamplingConfig {
199 pub max_new_tokens: usize,
200 pub temperature: f32,
201 pub top_p: f32,
203 pub top_k: usize,
205 pub cfg_scale: f32,
208 pub seed: u64,
209}
210
211impl SamplingConfig {
212 pub fn new(max_new_tokens: usize, seed: u64) -> Self {
215 Self {
216 max_new_tokens,
217 temperature: DEFAULT_TEMPERATURE,
218 top_p: 0.9,
219 top_k: 0,
220 cfg_scale: 2.0,
221 seed,
222 }
223 }
224}
225
226#[derive(Debug)]
229pub struct AceStepLm<B: Backend> {
230 pub model: Qwen3Model<B>,
231}
232
233impl<B: Backend> AceStepLm<B> {
234 pub fn new(config: &Qwen3Config, device: &B::Device) -> Self {
235 Self {
236 model: Qwen3Model::new(config, device),
237 }
238 }
239
240 pub fn from_burnpack(config: &Qwen3Config, path: &Path, device: &B::Device) -> Result<Self> {
241 Ok(Self {
242 model: Qwen3Model::from_burnpack(config, path, device)?,
243 })
244 }
245
246 pub fn from_burnpack_cast(
250 config: &Qwen3Config,
251 path: &Path,
252 device: &B::Device,
253 ) -> Result<Self> {
254 let mut model = Self::new(config, device);
255 let snapshots = burn_store::BurnpackStore::from_file(path)
256 .zero_copy(true)
257 .get_all_snapshots()
258 .with_context(|| format!("failed to read snapshots from {}", path.display()))?
259 .clone();
260 let mut converted = Vec::with_capacity(snapshots.len());
261 for snapshot in snapshots.values() {
262 let data = snapshot
263 .to_data()
264 .map_err(|e| anyhow::anyhow!("failed to decode {}: {e:?}", snapshot.full_path()))?
265 .convert::<B::FloatElem>();
266 converted.push(burn_store::TensorSnapshot::from_data(
267 data,
268 snapshot.path_stack.clone().unwrap_or_default(),
269 snapshot.container_stack.clone().unwrap_or_default(),
270 snapshot.tensor_id.unwrap_or_default(),
271 ));
272 }
273 let result =
274 burn_store::ModuleSnapshot::apply(&mut model.model, converted, None, None, false);
275 if !result.is_success() {
276 anyhow::bail!(
277 "failed to apply LM weights from {}: {result}",
278 path.display()
279 );
280 }
281 Ok(model)
282 }
283
284 pub fn generate_codes(
298 &self,
299 prompt_token_ids: &[u32],
300 uncond_token_ids: Option<&[u32]>,
301 vocab: &AudioCodeVocab,
302 sampling: &SamplingConfig,
303 mut progress: Option<&mut dyn FnMut(usize, usize)>,
304 ) -> Vec<u32> {
305 let device = self.model.embedding_weight().device();
306 let mut rng = SmallRng::seed_from_u64(sampling.seed);
307 let mut codes = Vec::new();
308 let use_cfg = sampling.cfg_scale > 1.0 && uncond_token_ids.is_some();
309
310 let mut cache = self.model.new_cache();
311 let prompt_len = prompt_token_ids.len();
312 let hidden =
313 self.model
314 .forward_cached(ids_tensor::<B>(prompt_token_ids, &device), &mut cache, 0);
315 let mut logits = self.last_position_logits(hidden);
316
317 let (mut uncond_cache, mut uncond_logits, uncond_len) = if use_cfg {
318 let uncond_ids = uncond_token_ids.expect("cfg requires uncond ids");
319 let mut cache = self.model.new_cache();
320 let hidden =
321 self.model
322 .forward_cached(ids_tensor::<B>(uncond_ids, &device), &mut cache, 0);
323 let logits = self.last_position_logits(hidden);
324 (Some(cache), Some(logits), uncond_ids.len())
325 } else {
326 (None, None, 0)
327 };
328
329 let mut completed = sampling.max_new_tokens;
330 let code_base = vocab.code_token_id(0);
333 for step in 0..sampling.max_new_tokens {
334 if let Some(cb) = progress.as_mut() {
335 cb(step, sampling.max_new_tokens);
336 }
337 let combined = match (&logits, &uncond_logits) {
338 (cond, Some(uncond)) => cond
339 .iter()
340 .zip(uncond.iter())
341 .map(|(c, u)| u + sampling.cfg_scale * (c - u))
342 .collect(),
343 (cond, None) => cond.clone(),
344 };
345 let token = sample_codes_token(&combined, code_base, sampling, &mut rng);
346 if token == IM_END_ID {
347 completed = step;
348 break;
349 }
350 if let Some(code) = vocab.token_id_to_code(token) {
351 codes.push(code);
352 }
353 let hidden = self.model.forward_cached(
354 ids_tensor::<B>(&[token], &device),
355 &mut cache,
356 prompt_len + step,
357 );
358 logits = self.last_position_logits(hidden);
359 if let (Some(cache), Some(uncond)) = (&mut uncond_cache, &mut uncond_logits) {
360 let hidden = self.model.forward_cached(
361 ids_tensor::<B>(&[token], &device),
362 cache,
363 uncond_len + step,
364 );
365 *uncond = self.last_position_logits(hidden);
366 }
367 }
368 if let Some(cb) = progress.as_mut() {
369 cb(completed, sampling.max_new_tokens);
370 }
371 codes
372 }
373
374 fn last_position_logits(&self, hidden: Tensor<B, 3>) -> Vec<f32> {
376 let [batch, seq_len, hidden_size] = hidden.dims();
377 let last = hidden
378 .slice([0..batch, seq_len - 1..seq_len, 0..hidden_size])
379 .reshape([batch, hidden_size]);
380 let weight = self.model.embedding_weight();
381 let logits = last.matmul(weight.swap_dims(0, 1));
382 logits
383 .cast(DType::F32)
384 .to_data()
385 .to_vec::<f32>()
386 .expect("lm_head logits should materialize as f32")
387 }
388}
389
390fn ids_tensor<B: Backend>(ids: &[u32], device: &B::Device) -> Tensor<B, 2, Int> {
391 let data: Vec<i64> = ids.iter().map(|&t| i64::from(t)).collect();
392 let len = data.len();
393 Tensor::<B, 2, Int>::from_data(TensorData::new(data, [1, len]), device)
394}
395
396fn sample_codes_token(
403 logits: &[f32],
404 code_base: Option<u32>,
405 sampling: &SamplingConfig,
406 rng: &mut SmallRng,
407) -> u32 {
408 let code_base = code_base.unwrap_or(IM_END_ID + 1) as usize;
409 let mut indices: Vec<usize> = Vec::with_capacity(logits.len() - code_base + 1);
410 if logits.len() > IM_END_ID as usize {
411 indices.push(IM_END_ID as usize);
412 indices.extend(code_base..logits.len());
413 } else {
414 indices.extend(0..logits.len());
416 }
417 indices.sort_by(|&a, &b| logits[b].total_cmp(&logits[a]));
418 if sampling.temperature < 1e-3 || sampling.top_k == 1 {
419 return indices[0] as u32;
420 }
421 if sampling.top_k > 0 {
422 indices.truncate(sampling.top_k.min(indices.len()).max(1));
423 }
424
425 let temperature = sampling.temperature.max(f32::EPSILON);
426 let max_scaled = logits[indices[0]] / temperature;
427 let mut weights: Vec<f32> = indices
428 .iter()
429 .map(|&i| (logits[i] / temperature - max_scaled).exp())
430 .collect();
431 let total: f32 = weights.iter().sum();
432
433 if sampling.top_p < 1.0 {
434 let mut keep = weights.len();
435 let mut acc = 0.0;
436 for (i, &w) in weights.iter().enumerate() {
437 acc += w;
438 if acc / total >= sampling.top_p {
439 keep = i + 1;
440 break;
441 }
442 }
443 weights.truncate(keep.max(1));
444 indices.truncate(keep.max(1));
445 }
446
447 let total: f32 = weights.iter().sum();
448 let mut draw = rng.random::<f32>() * total;
449 for (pos, &w) in weights.iter().enumerate() {
450 draw -= w;
451 if draw <= 0.0 {
452 return indices[pos] as u32;
453 }
454 }
455 *indices.last().expect("candidate set is non-empty") as u32
456}
457
458pub fn tokenize_prompt(tokenizer_json_path: &Path, text: &str) -> Result<Vec<u32>> {
466 let tokenizer = tokie::Tokenizer::from_json(tokenizer_json_path).map_err(|e| {
467 anyhow::anyhow!(
468 "failed to load tokenizer from {}: {e}",
469 tokenizer_json_path.display()
470 )
471 })?;
472 Ok(tokenizer.encode(text, false).ids)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use burn::backend::NdArray;
479
480 type TestBackend = NdArray<f32>;
481
482 fn testdata(name: &str) -> std::path::PathBuf {
483 Path::new(env!("CARGO_MANIFEST_DIR"))
484 .join("src/acestep/testdata")
485 .join(name)
486 }
487
488 #[test]
489 fn cot_block_all_fields() {
490 let block = build_cot_block(
491 "epic orchestral score",
492 Some(120.0),
493 Some("C major"),
494 Some("4/4"),
495 30,
496 );
497 assert_eq!(
498 block,
499 "<think>\n\
500 bpm: 120\n\
501 caption: epic orchestral score\n\
502 duration: 30\n\
503 keyscale: C major\n\
504 language: unknown\n\
505 timesignature: 4/4\n\
506 </think>"
507 );
508 }
509
510 #[test]
511 fn cot_block_missing_bpm() {
512 let block = build_cot_block("lofi beat", None, Some("A minor"), Some("4/4"), 10);
513 assert!(!block.contains("bpm:"));
514 assert_eq!(
515 block,
516 "<think>\n\
517 caption: lofi beat\n\
518 duration: 10\n\
519 keyscale: A minor\n\
520 language: unknown\n\
521 timesignature: 4/4\n\
522 </think>"
523 );
524 }
525
526 #[test]
527 fn cot_block_keeps_time_signature_verbatim() {
528 let block = build_cot_block("piano", None, None, Some("4/4"), 5);
529 assert!(block.contains("timesignature: 4/4\n"));
530 let block = build_cot_block("waltz", None, None, Some("6/8"), 5);
531 assert!(block.contains("timesignature: 6/8"));
532 let block = build_cot_block("march", Some(128.5), None, Some("3/4"), 5);
533 assert!(block.contains("timesignature: 3/4\n"));
534 assert!(block.contains("bpm: 128.5"));
535 }
536
537 #[test]
538 fn codes_prompt_byte_exact() {
539 let cot = "<think>\ncaption: calm piano\nlanguage: instrumental\n</think>";
540 let prompt = build_codes_prompt("calm piano", cot);
541 let expected = "<|im_start|>system\n\
542 # Instruction\n\
543 Generate audio semantic tokens based on the given conditions:\n\
544 \n\
545 <|im_end|>\n\
546 <|im_start|>user\n\
547 # Caption\n\
548 calm piano\n\
549 \n\
550 # Lyric\n\
551 [Instrumental]\n\
552 <|im_end|>\n\
553 <|im_start|>assistant\n\
554 <think>\n\
555 caption: calm piano\n\
556 language: instrumental\n\
557 </think>\n\
558 \n";
559 assert_eq!(prompt, expected);
560 }
561
562 #[test]
563 fn audio_code_vocab_from_tokenizer_json() {
564 let vocab = AudioCodeVocab::from_tokenizer_json(&testdata("lm_tokenizer_min.json"))
565 .expect("should parse minimal tokenizer.json");
566 assert_eq!(vocab.len(), 3);
567 assert!(!vocab.is_empty());
568 assert_eq!(vocab.code_token_id(0), Some(200));
569 assert_eq!(vocab.code_token_id(1), Some(201));
570 assert_eq!(vocab.code_token_id(42), Some(250));
571 assert_eq!(vocab.code_token_id(7), None);
572 assert_eq!(vocab.token_id_to_code(201), Some(1));
573 assert_eq!(vocab.token_id_to_code(101), None);
574 assert_eq!(AudioCodeVocab::IM_END_ID, 151_645);
575 assert_eq!(AudioCodeVocab::ENDOFTEXT_ID, 151_643);
576 }
577
578 #[test]
579 fn tokenize_prompt_maps_special_token_strings() {
580 let path = testdata("lm_tokenizer_min.json");
581 let ids = tokenize_prompt(&path, "<|im_start|>a<|im_end|>")
582 .expect("should tokenize with the minimal fixture");
583 assert_eq!(ids, vec![101, 0, 102]);
584 }
585
586 fn tiny_lm() -> AceStepLm<TestBackend> {
587 let config = Qwen3Config {
588 hidden_size: 32,
589 intermediate_size: 64,
590 num_hidden_layers: 2,
591 num_attention_heads: 4,
592 num_key_value_heads: 2,
593 head_dim: 8,
594 rms_norm_eps: 1e-6,
595 rope_theta: 1_000_000.0,
596 vocab_size: 128,
597 max_position_embeddings: 512,
598 tie_word_embeddings: true,
599 };
600 let device = Default::default();
601 AceStepLm::new(&config, &device)
602 }
603
604 fn tiny_vocab() -> AudioCodeVocab {
605 let mut vocab = AudioCodeVocab::default();
607 for code in 0..8u32 {
608 vocab.code_to_token.insert(code, code + 1);
609 vocab.token_to_code.insert(code + 1, code);
610 }
611 vocab
612 }
613
614 #[test]
615 fn generate_codes_deterministic_and_bounded() {
616 let lm = tiny_lm();
617 let vocab = tiny_vocab();
618 let prompt: Vec<u32> = vec![10, 11, 12, 13];
619
620 let mut calls = Vec::new();
621 let sampling = SamplingConfig::new(12, 42);
622 let first = {
623 let mut cb = |done: usize, total: usize| calls.push((done, total));
624 lm.generate_codes(&prompt, Some(&prompt), &vocab, &sampling, Some(&mut cb))
625 };
626 let second = lm.generate_codes(&prompt, Some(&prompt), &vocab, &sampling, None);
627
628 assert_eq!(first, second, "same seed must reproduce the same codes");
629 assert!(first.len() <= 12, "cannot exceed the token budget");
630 assert!(first.iter().all(|&c| c < 8), "codes stay in the toy vocab");
631 assert!(!calls.is_empty(), "progress callback should fire");
632 assert!(calls.iter().all(|&(_, total)| total == 12));
633 assert_eq!(calls.last(), Some(&(12, 12)), "budget-bound run completes");
634
635 let third = lm.generate_codes(&prompt, None, &vocab, &SamplingConfig::new(12, 7), None);
637 assert!(third.len() <= 12);
638 }
639}