8-gen bench (4 emotions × 2 corpora) at seed=42 against firdhokk Whisper-LV3: target RAVDESS CREMA-D happy happy (0.999) ✓ happy (0.999) ✓ angry neutral (0.92) sad (0.99) fearful happy (0.998) fearful (0.984) ✓ sad angry (0.99) fearful (0.99) CREMA-D 2/4 vs RAVDESS 1/4. Larger / more naturalistic corpus produces more class-pure fearful direction. Neither corpus solves angry or sad — recipe shifts into 'vague expressivity' rather than class-specific corners. Practical: prefer CREMA-D when available; A/B both per emotion if class precision matters. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
111 lines
3.8 KiB
Rust
111 lines
3.8 KiB
Rust
//! Demo: compute teacher-forced training loss for one frame on a real
|
|
//! (text, audio) pair using the loaded CSM-1B model.
|
|
//!
|
|
//! Pipeline:
|
|
//! 1. Load CSM-1B (FP path — required for backward through trainable params)
|
|
//! 2. Mimi-encode a reference WAV to get target codebook tokens
|
|
//! 3. Tokenize the matching transcript with Llama BPE
|
|
//! 4. Build the (1, S, 33) input tokens + mask
|
|
//! 5. Call `Model::forward_loss(tokens, mask, pos, target_codes)`
|
|
//! 6. Print the scalar cross-entropy
|
|
//!
|
|
//! This is the foundation primitive for LoRA fine-tuning. The next step is to
|
|
//! wrap the backbone q_proj/v_proj projections with `LoraLinear` (introducing
|
|
//! trainable params) and call `loss.backward()` followed by an AdamW step.
|
|
//!
|
|
//! Usage:
|
|
//! cargo run -p rtx-csm --release --example forward_loss_demo -- \
|
|
//! --wav /tmp/csm/hello.wav --text "Hello from Rust."
|
|
|
|
use anyhow::Result;
|
|
use clap::Parser;
|
|
use rtx_csm::{Generator, Segment, audio_io};
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Debug, Parser)]
|
|
struct Cli {
|
|
#[arg(long)]
|
|
wav: PathBuf,
|
|
#[arg(long)]
|
|
text: String,
|
|
#[arg(long, default_value_t = 0)]
|
|
speaker: u32,
|
|
#[arg(long)]
|
|
cpu: bool,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
tracing_subscriber::fmt().init();
|
|
let cli = Cli::parse();
|
|
|
|
let device = if cli.cpu {
|
|
candle_core::Device::Cpu
|
|
} else {
|
|
Generator::default_device()?
|
|
};
|
|
println!("device: {device:?}");
|
|
|
|
// FP backbone — required because forward_loss isn't implemented on the
|
|
// quantized backend (training-through-Q8 isn't a supported workflow).
|
|
let mut generator = Generator::load_csm_1b(&device)?;
|
|
|
|
// Audio side: load WAV, ensure 24 kHz mono, encode through Mimi.
|
|
let audio = audio_io::load_mono_24k(&cli.wav)?;
|
|
println!(
|
|
"loaded {} samples (~{:.2}s)",
|
|
audio.len(),
|
|
audio.len() as f32 / generator.config.sample_rate as f32
|
|
);
|
|
let codes = generator.mimi.encode(&audio)?;
|
|
let (b, cb, t_frames) = codes.dims3()?;
|
|
println!("Mimi codes shape: ({b}, {cb}, {t_frames})");
|
|
if t_frames < 1 {
|
|
anyhow::bail!("audio too short; need at least one Mimi frame");
|
|
}
|
|
|
|
// Take frame 0 as the target. Shape after narrow + flatten: (cb,) u32.
|
|
let target_codes = codes
|
|
.narrow(2, 0, 1)?
|
|
.squeeze(2)?
|
|
.flatten_all()?
|
|
.to_vec1::<u32>()?;
|
|
println!(
|
|
"target_codes[0..8] = {:?}",
|
|
&target_codes[..8.min(target_codes.len())]
|
|
);
|
|
|
|
// Build a "prompt + query" sequence, just like inference. The prompt is
|
|
// the transcript; we ask the model to predict the first audio frame given
|
|
// the text. Use Segment to leverage build_prompt.
|
|
let current = Segment::new_text(cli.speaker, &cli.text);
|
|
let prompt = rtx_csm::prompt::build_prompt(
|
|
&[],
|
|
¤t,
|
|
&generator.model,
|
|
&mut generator.mimi,
|
|
&generator.tokenizer,
|
|
)?;
|
|
println!("prompt tokens shape: {:?}", prompt.tokens.shape());
|
|
|
|
// Forward-loss for the first frame.
|
|
generator.model.clear_kv_cache();
|
|
let loss =
|
|
generator
|
|
.model
|
|
.inner
|
|
.forward_loss(&prompt.tokens, &prompt.mask, 0, &target_codes)?;
|
|
let loss_val = loss.to_scalar::<f32>()?;
|
|
println!("\nteacher-forced cross-entropy loss for frame 0: {loss_val:.4}");
|
|
|
|
// For context: a randomly initialized model would have CE ≈ ln(2051) ≈ 7.63.
|
|
// A perfectly-predicting model would have CE ≈ 0. Pretrained CSM should be
|
|
// somewhere in between for held-out audio.
|
|
let random_baseline = (generator.config.audio_vocab_size as f32).ln();
|
|
println!("random-baseline CE: {random_baseline:.4}");
|
|
println!(
|
|
"loss / random_baseline: {:.3} (lower = model is more confident)",
|
|
loss_val / random_baseline
|
|
);
|
|
Ok(())
|
|
}
|