diff --git a/rvsttd/src/main.rs b/rvsttd/src/main.rs index dbde51f..2c3523d 100644 --- a/rvsttd/src/main.rs +++ b/rvsttd/src/main.rs @@ -69,12 +69,112 @@ fn line_text(line: &transcript_line_t) -> String { .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, + log_lines: Vec, +} + +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 ───────────────────────────────────────────────────────── struct Shared { writer: Mutex, session_id: u64, transcriber_handle: i32, + debug_dir: Option, } impl Shared { @@ -175,6 +275,15 @@ fn transcriber_loop( let handle = shared.transcriber_handle; let mut sent_ids: HashSet = 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) { let chunk = { let mut buf = audio_buf.lock().unwrap(); @@ -186,6 +295,10 @@ fn transcriber_loop( std::mem::take(&mut *buf) }; + if let Some(ref mut r) = recorder { + r.add_audio(&chunk); + } + unsafe { moonshine_transcribe_add_audio_to_stream( handle, stream_handle, @@ -200,6 +313,10 @@ fn transcriber_loop( continue; } + if let Some(ref mut r) = recorder { + log_transcript_lines(r, t_ptr); + } + send_new_segments(&shared, t_ptr, &mut sent_ids, "P"); } @@ -207,6 +324,10 @@ fn transcriber_loop( if aborted.load(Ordering::SeqCst) { unsafe { moonshine_stop_stream(handle, stream_handle) }; 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; } @@ -215,6 +336,11 @@ fn transcriber_loop( let mut buf = audio_buf.lock().unwrap(); std::mem::take(&mut *buf) }; + + if let Some(ref mut r) = recorder { + r.add_audio(&remaining); + } + if !remaining.is_empty() { unsafe { moonshine_transcribe_add_audio_to_stream( @@ -234,6 +360,10 @@ fn transcriber_loop( let t = unsafe { &*t_ptr }; let mut new_segments: Vec = Vec::new(); + if let Some(ref mut r) = recorder { + log_transcript_lines(r, t_ptr); + } + for i in 0..t.line_count as usize { let line = unsafe { &*t.lines.add(i) }; if line.text.is_null() || line.is_complete == 0 { @@ -255,10 +385,32 @@ fn transcriber_loop( for (i, text) in new_segments.iter().enumerate() { let prefix = if i == last { "F" } else { "P" }; shared.send_msg(prefix, text); + if let Some(ref mut r) = recorder { + r.log_event(&format!("TX {} \"{}\"", prefix, text)); + } } } } else { 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 model_dir = default_model; + let mut debug = false; while let Some(a) = args.next() { match a.as_str() { "--model-dir" | "-m" => { model_dir = args.next().unwrap_or(model_dir); } + "--debug" => { + debug = true; + } "--help" | "-h" => { - println!("Usage: rvsttd [--model-dir DIR]"); + println!("Usage: rvsttd [--model-dir DIR] [--debug]"); println!(" rvsttd fetch [DIR]"); println!("Listens on TCP {}", BIND_ADDR); println!("Model: medium-streaming (Moonshine)"); + println!("--debug: save session audio + transcript log to ~/.rvsttd/debug/"); println!("'rvsttd fetch' downloads the English medium-streaming model"); 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) .unwrap_or_else(|_| std::path::PathBuf::from(&model_dir)); @@ -547,6 +713,7 @@ fn main() -> Result<()> { writer: Mutex::new(writer_stream.try_clone()?), session_id: new_session_id, transcriber_handle, + debug_dir: debug_dir.clone(), }); log(&format!("PTT on session {}", new_session_id));