rtx-csm: emotional_speech_guide — CREMA-D vs RAVDESS firdhokk verdict

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]>
This commit is contained in:
osobh
2026-04-30 00:01:02 -07:00
co-authored by Claude Opus 4.7
parent f4d8268381
commit a5cedfb46a
69 changed files with 1026 additions and 734 deletions
+44 -32
View File
@@ -14,7 +14,7 @@
use crate::audio_io;
use crate::error::{CsmError, Result};
use crate::generator::Generator;
use crate::prompt::{build_prompt, Segment};
use crate::prompt::{Segment, build_prompt};
use candle_core::Tensor;
use candle_nn::{AdamW, Optimizer, ParamsAdamW, VarMap};
use std::path::{Path, PathBuf};
@@ -121,10 +121,7 @@ impl TrainingDataset {
}
let txt_path = path.with_extension("txt");
if !txt_path.exists() {
tracing::warn!(
"skipping {}: no matching .txt transcript",
path.display()
);
tracing::warn!("skipping {}: no matching .txt transcript", path.display());
continue;
}
let text = std::fs::read_to_string(&txt_path)?.trim().to_string();
@@ -169,9 +166,8 @@ impl TrainingDataset {
if line.is_empty() || line.starts_with('#') {
continue;
}
let row: ManifestRow = serde_json::from_str(line).map_err(|e| {
CsmError::Config(format!("manifest line {}: {e}", lineno + 1))
})?;
let row: ManifestRow = serde_json::from_str(line)
.map_err(|e| CsmError::Config(format!("manifest line {}: {e}", lineno + 1)))?;
let text = row.transcript.trim().to_string();
if text.is_empty() {
tracing::warn!("manifest line {}: empty transcript, skipping", lineno + 1);
@@ -208,7 +204,9 @@ impl TrainingDataset {
ex.stage = row.stage;
tracing::debug!(
"manifest example: tag={:?} stage={:?} frames={}",
ex.emotion_tag, ex.stage, ex.frame_codes.len()
ex.emotion_tag,
ex.stage,
ex.frame_codes.len()
);
examples.push(ex);
}
@@ -442,7 +440,12 @@ impl<'a> CurriculumTrainer<'a> {
dataset: &'a TrainingDataset,
stages: Vec<CurriculumStage>,
) -> Self {
Self { generator, vm, dataset, stages }
Self {
generator,
vm,
dataset,
stages,
}
}
/// Run all stages sequentially. Returns one loss-trace `Vec<f32>` per
@@ -479,7 +482,9 @@ impl<'a> CurriculumTrainer<'a> {
stage.config.epochs,
stage.config.peak_lr,
);
let stage_dataset = TrainingDataset { examples: stage_examples };
let stage_dataset = TrainingDataset {
examples: stage_examples,
};
let mut trainer = Trainer::new(
self.generator,
self.vm,
@@ -619,10 +624,8 @@ pub fn evaluate_held_out(
if ex.frame_codes.is_empty() {
continue;
}
let prompt_text = crate::generator::apply_emotion_hint(
ex.text.clone(),
ex.emotion_tag.as_deref(),
);
let prompt_text =
crate::generator::apply_emotion_hint(ex.text.clone(), ex.emotion_tag.as_deref());
let current = Segment::new_text(ex.speaker, prompt_text);
let prompt = build_prompt(
&[],
@@ -640,20 +643,27 @@ pub fn evaluate_held_out(
let mut chosen = if frames_per_example == 0 {
(0..total_frames).collect::<Vec<_>>()
} else {
(0..n_sample).map(|_| rng.gen_range(0..total_frames)).collect()
(0..n_sample)
.map(|_| rng.gen_range(0..total_frames))
.collect()
};
chosen.sort();
let mut sum = 0.0f32;
for frame_idx in chosen.iter().copied() {
generator.model.clear_kv_cache();
let target = &ex.frame_codes[frame_idx];
let loss = generator
.model
.inner
.forward_loss(&prompt.tokens, &prompt.mask, 0, target)?;
let loss =
generator
.model
.inner
.forward_loss(&prompt.tokens, &prompt.mask, 0, target)?;
sum += loss.to_scalar::<f32>()?;
}
let mean = if n_sample > 0 { sum / n_sample as f32 } else { 0.0 };
let mean = if n_sample > 0 {
sum / n_sample as f32
} else {
0.0
};
rows.push(EvalRow {
idx,
source: ex.source.clone(),
@@ -714,9 +724,7 @@ impl LoraAdapterMetadata {
/// Read just the LoRA self-description from a safetensors header, without
/// loading any tensor data. Returns `None` when the file has no
/// `rtx_csm_lora` metadata key (older adapters trained before Phase 12.5).
pub fn read_lora_adapter_metadata<P: AsRef<Path>>(
path: P,
) -> Result<Option<LoraAdapterMetadata>> {
pub fn read_lora_adapter_metadata<P: AsRef<Path>>(path: P) -> Result<Option<LoraAdapterMetadata>> {
let data = std::fs::read(path.as_ref())?;
let (_n, meta) = safetensors::SafeTensors::read_metadata(&data)
.map_err(|e| CsmError::Other(anyhow::anyhow!("read safetensors header: {e}")))?;
@@ -728,9 +736,8 @@ pub fn read_lora_adapter_metadata<P: AsRef<Path>>(
Some(s) => s,
None => return Ok(None),
};
let parsed: LoraAdapterMetadata = serde_json::from_str(json).map_err(|e| {
CsmError::Other(anyhow::anyhow!("parse {LORA_METADATA_KEY}: {e}"))
})?;
let parsed: LoraAdapterMetadata = serde_json::from_str(json)
.map_err(|e| CsmError::Other(anyhow::anyhow!("parse {LORA_METADATA_KEY}: {e}")))?;
Ok(Some(parsed))
}
@@ -769,9 +776,7 @@ pub fn apply_lora_adapter<P: AsRef<Path>>(
);
}
let resolved_rank = rank
.or_else(|| meta.as_ref().map(|m| m.rank))
.unwrap_or(8);
let resolved_rank = rank.or_else(|| meta.as_ref().map(|m| m.rank)).unwrap_or(8);
let resolved_alpha = alpha
.or_else(|| meta.as_ref().map(|m| m.alpha))
.unwrap_or(16.0);
@@ -861,7 +866,11 @@ fn save_lora_adapter_inner<P: AsRef<Path>>(
tracing::info!(
"saved {n_tensors} LoRA tensors → {}{}",
out.as_ref().display(),
if header_meta.is_some() { " (with metadata)" } else { "" }
if header_meta.is_some() {
" (with metadata)"
} else {
""
}
);
Ok(())
}
@@ -885,7 +894,10 @@ pub fn load_lora_adapter<P: AsRef<Path>>(
tracing::warn!("safetensors has tensor `{name}` but VarMap has no matching Var");
}
}
tracing::info!("loaded {count} LoRA tensors from {}", path.as_ref().display());
tracing::info!(
"loaded {count} LoRA tensors from {}",
path.as_ref().display()
);
Ok(())
}