rvsttd: add --debug flag, save session audio (WAV) + transcript log
This commit is contained in:
+168
-1
@@ -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<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 ─────────────────────────────────────────────────────────
|
||||
|
||||
struct Shared {
|
||||
writer: Mutex<TcpStream>,
|
||||
session_id: u64,
|
||||
transcriber_handle: i32,
|
||||
debug_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
@@ -175,6 +275,15 @@ fn transcriber_loop(
|
||||
let handle = shared.transcriber_handle;
|
||||
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) {
|
||||
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<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 {
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user