murmur_core/transcription/
subprocess.rs1use anyhow::{Context, Result};
9use std::io::{Read, Write};
10use std::path::{Path, PathBuf};
11use std::process::{Child, Command, Stdio};
12
13pub struct SubprocessTranscriber {
15 child: Child,
16}
17
18impl SubprocessTranscriber {
19 pub fn new(model_path: &Path, language: &str) -> Result<Self> {
24 let worker_path = find_worker_binary()?;
25 log::info!(
26 "Spawning whisper worker: {} (model: {})",
27 worker_path.display(),
28 model_path.display()
29 );
30
31 let child = Command::new(&worker_path)
32 .arg(model_path.to_str().context("Invalid model path")?)
33 .arg(language)
34 .stdin(Stdio::piped())
35 .stdout(Stdio::piped())
36 .stderr(Stdio::inherit())
37 .spawn()
38 .context("Failed to spawn murmur-whisper-worker")?;
39
40 Ok(Self { child })
41 }
42
43 pub fn transcribe(&mut self, samples: &[f32], translate: bool) -> Result<String> {
45 let stdin = self
46 .child
47 .stdin
48 .as_mut()
49 .context("Worker stdin not available")?;
50 let stdout = self
51 .child
52 .stdout
53 .as_mut()
54 .context("Worker stdout not available")?;
55
56 let count = (samples.len() as u32).to_le_bytes();
58 stdin.write_all(&count)?;
59
60 let sample_bytes: &[u8] =
61 unsafe { std::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 4) };
62 stdin.write_all(sample_bytes)?;
63
64 let flag = [if translate { 1u8 } else { 0u8 }];
65 stdin.write_all(&flag)?;
66 stdin.flush()?;
67
68 let mut len_buf = [0u8; 4];
70 stdout
71 .read_exact(&mut len_buf)
72 .context("Failed to read response length from worker")?;
73 let text_len = u32::from_le_bytes(len_buf) as usize;
74
75 if text_len == 0 {
76 return Ok(String::new());
77 }
78
79 let mut text_buf = vec![0u8; text_len];
80 stdout
81 .read_exact(&mut text_buf)
82 .context("Failed to read response text from worker")?;
83
84 String::from_utf8(text_buf).context("Worker returned invalid UTF-8")
85 }
86
87 pub fn shutdown(&mut self) {
89 if let Some(ref mut stdin) = self.child.stdin {
90 let _ = stdin.write_all(&0u32.to_le_bytes());
91 let _ = stdin.flush();
92 }
93 let _ = self.child.wait();
94 }
95}
96
97impl Drop for SubprocessTranscriber {
98 fn drop(&mut self) {
99 self.shutdown();
100 }
101}
102
103fn find_worker_binary() -> Result<PathBuf> {
105 let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
106 let exe_dir = current_exe
107 .parent()
108 .context("Executable has no parent directory")?;
109
110 let worker_name = if cfg!(windows) {
111 "murmur-whisper-worker.exe"
112 } else {
113 "murmur-whisper-worker"
114 };
115
116 let worker_path = exe_dir.join(worker_name);
117 anyhow::ensure!(
118 worker_path.exists(),
119 "murmur-whisper-worker not found at {}. Build it with: cargo build -p murmur-core --bin murmur-whisper-worker",
120 worker_path.display()
121 );
122 Ok(worker_path)
123}