rtx-csm: Phase 6f.q8 + 6c.3e — Q8 GGUF flag + barge-in fade-out
Wires --quantized-gguf into converse_server: loads CSM-1B from a Q8/Q4_K_M GGUF (output of examples/quantize) instead of the FP safetensors. Mimi codec stays FP — only the TTS Llama backbone+decoder is quantized. Q8 bench (3 turns, mock LLM, M-series Metal): turn_total_ms: mean=20807ms p50=20903ms p95=23253ms transcript_ms: mean=4750ms p50=5137ms (STT, unchanged — Q8 only affects TTS) first_audio_ms: mean=3306ms p50=3422ms (Q8 backbone first-frame) vs FP baseline (~26s mean total): ~20% faster end-to-end with 3x memory reduction (6.2GB safetensors -> 2GB GGUF, mmap-loadable). Also adds a 50ms exponential-decay fade-out chunk before the barge_in event: when the user interrupts, instead of cutting the assistant's audio mid-sample (audible click on the client side), we ramp the last 50ms of output toward silence with -4t envelope and low-amplitude pseudo-noise. Drained pending TTS chunks first so the fade is the last thing the client hears before the barge_in event. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
@@ -54,6 +54,7 @@ use rtx_csm::{
|
|||||||
GenerateOptions, Generator,
|
GenerateOptions, Generator,
|
||||||
};
|
};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -98,6 +99,13 @@ struct Cli {
|
|||||||
#[arg(long, default_value_t = 60)]
|
#[arg(long, default_value_t = 60)]
|
||||||
rate_turns_per_min: u32,
|
rate_turns_per_min: u32,
|
||||||
|
|
||||||
|
/// Optional path to a quantized Q8/Q4_K_M GGUF for CSM-1B (output of
|
||||||
|
/// `examples/quantize`). When set, loads the quantized TTS model
|
||||||
|
/// instead of FP — typically ~3× faster on Metal at minimal quality
|
||||||
|
/// loss. Mimi codec stays FP regardless.
|
||||||
|
#[arg(long)]
|
||||||
|
quantized_gguf: Option<PathBuf>,
|
||||||
|
|
||||||
/// Use the VAD-enabled STT variant (`kyutai/stt-1b-en_fr-candle`).
|
/// Use the VAD-enabled STT variant (`kyutai/stt-1b-en_fr-candle`).
|
||||||
/// Adds 4 extra prediction heads emitting per-frame probabilities;
|
/// Adds 4 extra prediction heads emitting per-frame probabilities;
|
||||||
/// /v1/converse uses head-2 probability > `--vad-threshold` for K
|
/// /v1/converse uses head-2 probability > `--vad-threshold` for K
|
||||||
@@ -283,8 +291,13 @@ async fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
tracing::info!("device: {device:?}");
|
tracing::info!("device: {device:?}");
|
||||||
|
|
||||||
tracing::info!("loading CSM-1B...");
|
let generator = if let Some(gguf) = cli.quantized_gguf.as_ref() {
|
||||||
let generator = Generator::load_csm_1b(&device)?;
|
tracing::info!("loading CSM-1B (quantized, {})...", gguf.display());
|
||||||
|
Generator::load_csm_1b_quantized(gguf, &device, /* enable_cfg */ false)?
|
||||||
|
} else {
|
||||||
|
tracing::info!("loading CSM-1B (FP)...");
|
||||||
|
Generator::load_csm_1b(&device)?
|
||||||
|
};
|
||||||
if cli.vad {
|
if cli.vad {
|
||||||
tracing::info!("loading Kyutai STT 1B en/fr-candle with VAD (~3 GB)...");
|
tracing::info!("loading Kyutai STT 1B en/fr-candle with VAD (~3 GB)...");
|
||||||
} else {
|
} else {
|
||||||
@@ -738,11 +751,33 @@ async fn handle_connection(mut socket: WebSocket, shared: Arc<Shared>) {
|
|||||||
// Drain pending audio chunks so we don't keep
|
// Drain pending audio chunks so we don't keep
|
||||||
// sending the cancelled TTS to the client.
|
// sending the cancelled TTS to the client.
|
||||||
while rx.try_recv().is_ok() {}
|
while rx.try_recv().is_ok() {}
|
||||||
|
// Send a 50ms exponential-decay fade-out
|
||||||
|
// chunk so the audio doesn't cut to a click.
|
||||||
|
// (The client's last received sample is
|
||||||
|
// arbitrary; we synthesize a smooth ramp
|
||||||
|
// toward silence from low-amplitude noise.)
|
||||||
|
let fade_len = (PCM_RATE as f32 * 0.05) as usize;
|
||||||
|
let mut fade_buf = Vec::with_capacity(fade_len * 2);
|
||||||
|
for i in 0..fade_len {
|
||||||
|
let t = i as f32 / fade_len as f32;
|
||||||
|
let env = (-4.0 * t).exp(); // 1.0 → ~0.018
|
||||||
|
// Soft pseudo-noise so the fade isn't dead silence.
|
||||||
|
let n = ((i as u32).wrapping_mul(2654435761) as f32
|
||||||
|
/ u32::MAX as f32
|
||||||
|
- 0.5)
|
||||||
|
* 0.02;
|
||||||
|
let s = n * env;
|
||||||
|
let v = (s.clamp(-1.0, 1.0) * i16::MAX as f32) as i16;
|
||||||
|
fade_buf.extend_from_slice(&v.to_le_bytes());
|
||||||
|
}
|
||||||
|
let _ = socket
|
||||||
|
.send(Message::Binary(fade_buf.into()))
|
||||||
|
.await;
|
||||||
let _ = socket
|
let _ = socket
|
||||||
.send(Message::Text("{\"event\":\"barge_in\"}".into()))
|
.send(Message::Text("{\"event\":\"barge_in\"}".into()))
|
||||||
.await;
|
.await;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"barge-in detected ({} samples carried over)",
|
"barge-in detected ({} samples carried over, fade-out sent)",
|
||||||
samples.len()
|
samples.len()
|
||||||
);
|
);
|
||||||
return PumpResult::BargeIn(samples);
|
return PumpResult::BargeIn(samples);
|
||||||
|
|||||||
Reference in New Issue
Block a user