A new model crate at crates/models/rtx-csm implementing end-to-end inference, quantization, and fine-tuning for Sesame's Conversational Speech Model (CSM-1B). Built on candle 0.9 + Kyutai Mimi codec. Key capabilities: - Inference (FP F16 on Metal, F32 on CPU, BF16 on CUDA) - Quantized inference (Q8_0 / Q4_K_M GGUF, ~3x speedup, ~50% memory) - Streaming Mimi decode with proper StreamTensor state machine - In-context voice cloning via SpeakerProfile - Classifier-Free Guidance (Koel-TTS recipe) - Long-form chunked generation with rolling context - Audio post-processing (HPF + declick + EBU R128 LUFS) - Text input normalization (brackets, times, unicode, length caps) - Frame-level repetition guard (loop-escape) - Top-k + top-p sampling - LoRA fine-tuning end-to-end (training + inference, on FP and Q8 bases) - In-process Whisper ASR via whisper-rs (under --features asr) - Standalone TTS HTTP server (Axum) - Bench harness with manifest export + per-prompt WER Phases delivered: quantization, ASR/WER eval, LoRA voice cloning, HTTP service. AudioSeal/WavLM/Unmute remain as documented future work. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
173 lines
5.3 KiB
Rust
173 lines
5.3 KiB
Rust
//! Frame-level repetition detection ("loop-escape").
|
|
//!
|
|
//! CSM's known pathological mode is **syllable looping** — the AR decoder
|
|
//! gets stuck in a low-entropy attractor and emits the same 32-codebook
|
|
//! frame over and over. Users hear it as a syllable repeating forever.
|
|
//!
|
|
//! The ideal fix is per-codebook logit blocking on recent frame
|
|
//! fingerprints, but the actual sampling happens *inside* candle's
|
|
//! `csm::Model::generate_frame` and we can't hook between codebook
|
|
//! samples without forking the upstream module. So instead we detect the
|
|
//! loop *after* the frame is emitted and short-circuit generation.
|
|
//!
|
|
//! This catches both:
|
|
//! - period-1 loops (frame_k == frame_{k-1}, ...)
|
|
//! - higher-period loops where a small set of frames cycles
|
|
//!
|
|
//! Detection rule: within the last `window` emitted frames, if any single
|
|
//! fingerprint occurs `max_repeats` or more times, we declare a loop.
|
|
|
|
use std::collections::{HashMap, VecDeque};
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct RepetitionConfig {
|
|
/// How many recent frames to track. 16 frames = 1.28s at 12.5 Hz.
|
|
pub window: usize,
|
|
/// A single fingerprint occurring this many times in the window triggers
|
|
/// a loop break. 4 is a safe default — natural speech can have 2-3
|
|
/// repeated frames at a sustained vowel without being pathological.
|
|
pub max_repeats: usize,
|
|
}
|
|
|
|
impl Default for RepetitionConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
window: 16,
|
|
max_repeats: 4,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tracks recent frame fingerprints. Cheap (a small ring buffer of u64).
|
|
pub struct RepetitionGuard {
|
|
cfg: RepetitionConfig,
|
|
recent: VecDeque<u64>,
|
|
}
|
|
|
|
impl RepetitionGuard {
|
|
pub fn new(cfg: RepetitionConfig) -> Self {
|
|
Self {
|
|
cfg,
|
|
recent: VecDeque::with_capacity(cfg.window),
|
|
}
|
|
}
|
|
|
|
pub fn reset(&mut self) {
|
|
self.recent.clear();
|
|
}
|
|
|
|
/// Record a frame and return true if a loop is detected. Frame is the
|
|
/// `Vec<u32>` of length num_codebooks returned by `generate_frame`.
|
|
pub fn observe(&mut self, frame: &[u32]) -> bool {
|
|
let fp = fingerprint(frame);
|
|
if self.recent.len() == self.cfg.window {
|
|
self.recent.pop_front();
|
|
}
|
|
self.recent.push_back(fp);
|
|
// Count occurrences only when the window has enough frames to make
|
|
// the threshold meaningful — avoids spurious early triggers.
|
|
if self.recent.len() < self.cfg.max_repeats {
|
|
return false;
|
|
}
|
|
let mut counts: HashMap<u64, usize> = HashMap::new();
|
|
let mut max_count = 0usize;
|
|
for h in self.recent.iter() {
|
|
let c = counts.entry(*h).or_insert(0);
|
|
*c += 1;
|
|
if *c > max_count {
|
|
max_count = *c;
|
|
}
|
|
}
|
|
max_count >= self.cfg.max_repeats
|
|
}
|
|
}
|
|
|
|
/// FNV-1a 64-bit hash over the codebook ids. Fast, avoids extra deps.
|
|
fn fingerprint(frame: &[u32]) -> u64 {
|
|
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
|
|
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
|
|
let mut h = FNV_OFFSET;
|
|
for v in frame {
|
|
let bytes = v.to_le_bytes();
|
|
for b in bytes {
|
|
h ^= b as u64;
|
|
h = h.wrapping_mul(FNV_PRIME);
|
|
}
|
|
}
|
|
h
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn no_repeats_no_trigger() {
|
|
let mut g = RepetitionGuard::new(RepetitionConfig::default());
|
|
for i in 0..20u32 {
|
|
let frame: Vec<u32> = (0..32).map(|c| i.wrapping_add(c)).collect();
|
|
assert!(!g.observe(&frame), "false trigger at frame {i}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn period_1_loop_trips_after_max_repeats() {
|
|
let mut g = RepetitionGuard::new(RepetitionConfig {
|
|
window: 16,
|
|
max_repeats: 4,
|
|
});
|
|
let frame = vec![42u32; 32];
|
|
// Three identical frames must NOT trigger.
|
|
assert!(!g.observe(&frame));
|
|
assert!(!g.observe(&frame));
|
|
assert!(!g.observe(&frame));
|
|
// Fourth one trips the guard.
|
|
assert!(g.observe(&frame));
|
|
}
|
|
|
|
#[test]
|
|
fn period_2_alternating_loop_trips() {
|
|
let mut g = RepetitionGuard::new(RepetitionConfig {
|
|
window: 16,
|
|
max_repeats: 4,
|
|
});
|
|
let a = vec![1u32; 32];
|
|
let b = vec![2u32; 32];
|
|
let mut tripped_at = None;
|
|
for i in 0..16 {
|
|
let f = if i % 2 == 0 { &a } else { &b };
|
|
if g.observe(f) {
|
|
tripped_at = Some(i);
|
|
break;
|
|
}
|
|
}
|
|
assert!(tripped_at.is_some(), "alternating loop never tripped");
|
|
}
|
|
|
|
#[test]
|
|
fn fingerprint_distinguishes_neighbors() {
|
|
let a: Vec<u32> = (0..32).collect();
|
|
let mut b = a.clone();
|
|
b[5] = 999;
|
|
assert_ne!(fingerprint(&a), fingerprint(&b));
|
|
}
|
|
|
|
#[test]
|
|
fn reset_clears_history() {
|
|
let mut g = RepetitionGuard::new(RepetitionConfig {
|
|
window: 16,
|
|
max_repeats: 4,
|
|
});
|
|
let frame = vec![42u32; 32];
|
|
for _ in 0..5 {
|
|
g.observe(&frame);
|
|
}
|
|
g.reset();
|
|
// After reset, we need 4 more observations to trip again.
|
|
assert!(!g.observe(&frame));
|
|
assert!(!g.observe(&frame));
|
|
assert!(!g.observe(&frame));
|
|
assert!(g.observe(&frame));
|
|
}
|
|
}
|