rvsttd: add --debug flag, save session audio (WAV) + transcript log

This commit is contained in:
2026-08-14 06:39:58 +00:00
parent 8473ab6ba5
commit 004cd10f78
+168 -1
View File
@@ -69,12 +69,112 @@ fn line_text(line: &transcript_line_t) -> String {
.into_owned() .into_owned()
} }
fn unix_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
// ─── debug session recorder ──────────────────────────────────────────────
struct DebugRecorder {
dir: PathBuf,
audio: Vec<f32>,
log_lines: Vec<String>,
}
impl DebugRecorder {
fn new(base_dir: &PathBuf) -> Self {
let dir = base_dir.join(unix_now().to_string());
std::fs::create_dir_all(&dir).ok();
Self {
dir,
audio: Vec::new(),
log_lines: Vec::new(),
}
}
fn log_event(&mut self, event: &str) {
self.log_lines.push(format!("[{}] {}", ts(), event));
}
fn log_segment(&mut self, line: &transcript_line_t, prefix: &str) {
let text = line_text(line);
let event = format!(
"SEGMENT id={} prefix={} is_complete={} start={:.3}s duration={:.3}s text=\"{}\"",
line.id, prefix, line.is_complete, line.start_time, line.duration, text
);
self.log_event(&event);
}
fn add_audio(&mut self, samples: &[f32]) {
self.audio.extend_from_slice(samples);
}
fn save(self) {
// Write log
let log_path = self.dir.join("session.log");
match std::fs::File::create(&log_path) {
Ok(mut f) => {
for line in &self.log_lines {
writeln!(f, "{}", line).ok();
}
}
Err(e) => log(&format!("debug: failed to write log: {}", e)),
}
// Write WAV
let wav_path = self.dir.join("audio.wav");
match write_wav(&wav_path, &self.audio, SAMPLE_RATE as u32) {
Ok(()) => {
let secs = self.audio.len() as f64 / SAMPLE_RATE as f64;
log(&format!("debug: saved {} ({:.1}s, {} samples)", self.dir.display(), secs, self.audio.len()));
}
Err(e) => log(&format!("debug: failed to write wav: {}", e)),
}
}
}
fn write_wav(path: &PathBuf, samples: &[f32], sample_rate: u32) -> Result<()> {
let num_samples = samples.len() as u32;
let data_size = num_samples * 4; // f32 = 4 bytes
let file_size = 44 + data_size;
let mut f = std::fs::File::create(path)?;
// RIFF header
f.write_all(b"RIFF")?;
f.write_all(&(file_size - 8).to_le_bytes())?;
f.write_all(b"WAVE")?;
// fmt chunk
f.write_all(b"fmt ")?;
f.write_all(&16u32.to_le_bytes())?; // chunk size
f.write_all(&3u16.to_le_bytes())?; // IEEE float
f.write_all(&1u16.to_le_bytes())?; // mono
f.write_all(&sample_rate.to_le_bytes())?;
f.write_all(&(sample_rate * 4).to_le_bytes())?; // byte rate
f.write_all(&4u16.to_le_bytes())?; // block align
f.write_all(&32u16.to_le_bytes())?; // bits per sample
// data chunk
f.write_all(b"data")?;
f.write_all(&data_size.to_le_bytes())?;
// Convert f32 samples to little-endian bytes
let mut bytes = Vec::with_capacity(data_size as usize);
for &s in samples {
bytes.extend_from_slice(&s.to_le_bytes());
}
f.write_all(&bytes)?;
Ok(())
}
// ─── shared state ───────────────────────────────────────────────────────── // ─── shared state ─────────────────────────────────────────────────────────
struct Shared { struct Shared {
writer: Mutex<TcpStream>, writer: Mutex<TcpStream>,
session_id: u64, session_id: u64,
transcriber_handle: i32, transcriber_handle: i32,
debug_dir: Option<PathBuf>,
} }
impl Shared { impl Shared {
@@ -175,6 +275,15 @@ fn transcriber_loop(
let handle = shared.transcriber_handle; let handle = shared.transcriber_handle;
let mut sent_ids: HashSet<u64> = HashSet::new(); let mut sent_ids: HashSet<u64> = HashSet::new();
// Debug recorder (if enabled)
let mut recorder = shared.debug_dir.as_ref().map(|_| DebugRecorder::new(
&shared.debug_dir.as_ref().unwrap().join(shared.session_id.to_string()),
));
if let Some(ref mut r) = recorder {
r.log_event(&format!("session {} started", shared.session_id));
}
while !stop_signal.load(Ordering::SeqCst) { while !stop_signal.load(Ordering::SeqCst) {
let chunk = { let chunk = {
let mut buf = audio_buf.lock().unwrap(); let mut buf = audio_buf.lock().unwrap();
@@ -186,6 +295,10 @@ fn transcriber_loop(
std::mem::take(&mut *buf) std::mem::take(&mut *buf)
}; };
if let Some(ref mut r) = recorder {
r.add_audio(&chunk);
}
unsafe { unsafe {
moonshine_transcribe_add_audio_to_stream( moonshine_transcribe_add_audio_to_stream(
handle, stream_handle, handle, stream_handle,
@@ -200,6 +313,10 @@ fn transcriber_loop(
continue; continue;
} }
if let Some(ref mut r) = recorder {
log_transcript_lines(r, t_ptr);
}
send_new_segments(&shared, t_ptr, &mut sent_ids, "P"); send_new_segments(&shared, t_ptr, &mut sent_ids, "P");
} }
@@ -207,6 +324,10 @@ fn transcriber_loop(
if aborted.load(Ordering::SeqCst) { if aborted.load(Ordering::SeqCst) {
unsafe { moonshine_stop_stream(handle, stream_handle) }; unsafe { moonshine_stop_stream(handle, stream_handle) };
log(&format!("Session {} aborted, skipping final flush", shared.session_id)); log(&format!("Session {} aborted, skipping final flush", shared.session_id));
if let Some(mut r) = recorder {
r.log_event("aborted (new session took over)");
r.save();
}
return; return;
} }
@@ -215,6 +336,11 @@ fn transcriber_loop(
let mut buf = audio_buf.lock().unwrap(); let mut buf = audio_buf.lock().unwrap();
std::mem::take(&mut *buf) std::mem::take(&mut *buf)
}; };
if let Some(ref mut r) = recorder {
r.add_audio(&remaining);
}
if !remaining.is_empty() { if !remaining.is_empty() {
unsafe { unsafe {
moonshine_transcribe_add_audio_to_stream( moonshine_transcribe_add_audio_to_stream(
@@ -234,6 +360,10 @@ fn transcriber_loop(
let t = unsafe { &*t_ptr }; let t = unsafe { &*t_ptr };
let mut new_segments: Vec<String> = Vec::new(); let mut new_segments: Vec<String> = Vec::new();
if let Some(ref mut r) = recorder {
log_transcript_lines(r, t_ptr);
}
for i in 0..t.line_count as usize { for i in 0..t.line_count as usize {
let line = unsafe { &*t.lines.add(i) }; let line = unsafe { &*t.lines.add(i) };
if line.text.is_null() || line.is_complete == 0 { if line.text.is_null() || line.is_complete == 0 {
@@ -255,10 +385,32 @@ fn transcriber_loop(
for (i, text) in new_segments.iter().enumerate() { for (i, text) in new_segments.iter().enumerate() {
let prefix = if i == last { "F" } else { "P" }; let prefix = if i == last { "F" } else { "P" };
shared.send_msg(prefix, text); shared.send_msg(prefix, text);
if let Some(ref mut r) = recorder {
r.log_event(&format!("TX {} \"{}\"", prefix, text));
}
} }
} }
} else { } else {
shared.send_msg("F", ""); shared.send_msg("F", "");
if let Some(ref mut r) = recorder {
r.log_event("TX F (empty)");
}
}
if let Some(mut r) = recorder {
r.log_event("session ended");
r.save();
}
}
fn log_transcript_lines(recorder: &mut DebugRecorder, t_ptr: *const transcript_t) {
let t = unsafe { &*t_ptr };
for i in 0..t.line_count as usize {
let line = unsafe { &*t.lines.add(i) };
if line.text.is_null() {
continue;
}
recorder.log_segment(line, if line.is_complete != 0 { "complete" } else { "partial" });
} }
} }
@@ -468,17 +620,22 @@ fn main() -> Result<()> {
let mut args = first.into_iter().chain(args); let mut args = first.into_iter().chain(args);
let mut model_dir = default_model; let mut model_dir = default_model;
let mut debug = false;
while let Some(a) = args.next() { while let Some(a) = args.next() {
match a.as_str() { match a.as_str() {
"--model-dir" | "-m" => { "--model-dir" | "-m" => {
model_dir = args.next().unwrap_or(model_dir); model_dir = args.next().unwrap_or(model_dir);
} }
"--debug" => {
debug = true;
}
"--help" | "-h" => { "--help" | "-h" => {
println!("Usage: rvsttd [--model-dir DIR]"); println!("Usage: rvsttd [--model-dir DIR] [--debug]");
println!(" rvsttd fetch [DIR]"); println!(" rvsttd fetch [DIR]");
println!("Listens on TCP {}", BIND_ADDR); println!("Listens on TCP {}", BIND_ADDR);
println!("Model: medium-streaming (Moonshine)"); println!("Model: medium-streaming (Moonshine)");
println!("--debug: save session audio + transcript log to ~/.rvsttd/debug/");
println!("'rvsttd fetch' downloads the English medium-streaming model"); println!("'rvsttd fetch' downloads the English medium-streaming model");
return Ok(()); return Ok(());
} }
@@ -486,6 +643,15 @@ fn main() -> Result<()> {
} }
} }
let debug_dir = if debug {
let d = PathBuf::from(&home).join(".rvsttd").join("debug");
std::fs::create_dir_all(&d).ok();
log(&format!("Debug mode enabled — sessions saved to {}", d.display()));
Some(d)
} else {
None
};
let model_path = std::fs::canonicalize(&model_dir) let model_path = std::fs::canonicalize(&model_dir)
.unwrap_or_else(|_| std::path::PathBuf::from(&model_dir)); .unwrap_or_else(|_| std::path::PathBuf::from(&model_dir));
@@ -547,6 +713,7 @@ fn main() -> Result<()> {
writer: Mutex::new(writer_stream.try_clone()?), writer: Mutex::new(writer_stream.try_clone()?),
session_id: new_session_id, session_id: new_session_id,
transcriber_handle, transcriber_handle,
debug_dir: debug_dir.clone(),
}); });
log(&format!("PTT on session {}", new_session_id)); log(&format!("PTT on session {}", new_session_id));