voicepeak_cli/
voicepeak.rs1use std::process::{Command as ProcessCommand, Output, Stdio};
2use std::sync::{mpsc, Arc, Mutex};
3use std::thread;
4use std::time::Duration;
5
6const VOICEPEAK_PATH: &str = "/Applications/voicepeak.app/Contents/MacOS/voicepeak";
7
8pub fn list_narrator() {
9 let output = ProcessCommand::new(VOICEPEAK_PATH)
10 .arg("--list-narrator")
11 .output();
12
13 match output {
14 Ok(output) => {
15 print!("{}", String::from_utf8_lossy(&output.stdout));
16 }
17 Err(e) => {
18 eprintln!("Failed to execute voicepeak: {}", e);
19 }
20 }
21}
22
23pub fn list_emotion(narrator: &str) {
24 let output = ProcessCommand::new(VOICEPEAK_PATH)
25 .arg("--list-emotion")
26 .arg(narrator)
27 .output();
28
29 match output {
30 Ok(output) => {
31 print!("{}", String::from_utf8_lossy(&output.stdout));
32 }
33 Err(e) => {
34 eprintln!("Failed to execute voicepeak: {}", e);
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
40struct CommandArgs {
41 text: Option<String>,
42 narrator: Option<String>,
43 emotion: Option<String>,
44 output: Option<std::path::PathBuf>,
45 speed: Option<String>,
46 pitch: Option<String>,
47}
48
49pub struct VoicepeakCommand {
50 args: CommandArgs,
51}
52
53fn execute_command_with_timeout(
54 mut command: ProcessCommand,
55 timeout_secs: u64,
56) -> Result<Output, Box<dyn std::error::Error>> {
57 let (tx, rx) = mpsc::channel();
58 let child_id = Arc::new(Mutex::new(None::<u32>));
59 let child_id_clone = Arc::clone(&child_id);
60
61 thread::spawn(move || {
62 let child = match command
63 .stdout(Stdio::piped())
64 .stderr(Stdio::piped())
65 .spawn()
66 {
67 Ok(child) => child,
68 Err(e) => {
69 let _ = tx.send(Err(e));
70 return;
71 }
72 };
73
74 {
76 let mut id_guard = child_id_clone.lock().unwrap();
77 *id_guard = Some(child.id());
78 }
79
80 let result = child.wait_with_output();
81 let _ = tx.send(result);
82 });
83
84 match rx.recv_timeout(Duration::from_secs(timeout_secs)) {
85 Ok(result) => result.map_err(|e| e.into()),
86 Err(_) => {
87 if let Ok(id_guard) = child_id.lock() {
89 if let Some(pid) = *id_guard {
90 kill_process_by_id(pid);
91 }
92 }
93 Err("Command timed out after 15 seconds".into())
94 }
95 }
96}
97
98fn kill_process_by_id(pid: u32) {
99 use std::process::Command;
100 let _ = Command::new("kill").arg("-9").arg(pid.to_string()).output();
101}
102
103fn kill_all_voicepeak_processes() {
104 use std::process::Command;
105 let _ = Command::new("pkill").arg("-f").arg("voicepeak").output();
106}
107
108impl VoicepeakCommand {
109 pub fn new() -> Self {
110 Self {
111 args: CommandArgs {
112 text: None,
113 narrator: None,
114 emotion: None,
115 output: None,
116 speed: None,
117 pitch: None,
118 },
119 }
120 }
121
122 pub fn text(mut self, text: &str) -> Self {
123 self.args.text = Some(text.to_string());
124 self
125 }
126
127 pub fn narrator(mut self, narrator: &str) -> Self {
128 self.args.narrator = Some(narrator.to_string());
129 self
130 }
131
132 pub fn emotion(mut self, emotion: &str) -> Self {
133 if !emotion.is_empty() {
134 self.args.emotion = Some(emotion.to_string());
135 }
136 self
137 }
138
139 pub fn output(mut self, path: &std::path::Path) -> Self {
140 self.args.output = Some(path.to_path_buf());
141 self
142 }
143
144 pub fn speed(mut self, speed: &str) -> Self {
145 self.args.speed = Some(speed.to_string());
146 self
147 }
148
149 pub fn pitch(mut self, pitch: &str) -> Self {
150 self.args.pitch = Some(pitch.to_string());
151 self
152 }
153
154 fn build_command(&self) -> ProcessCommand {
155 let mut command = ProcessCommand::new(VOICEPEAK_PATH);
156
157 if let Some(ref text) = self.args.text {
158 command.arg("-s").arg(text);
159 }
160 if let Some(ref narrator) = self.args.narrator {
161 command.arg("-n").arg(narrator);
162 }
163 if let Some(ref emotion) = self.args.emotion {
164 command.arg("-e").arg(emotion);
165 }
166 if let Some(ref output) = self.args.output {
167 command.arg("-o").arg(output);
168 }
169 if let Some(ref speed) = self.args.speed {
170 command.arg("--speed").arg(speed);
171 }
172 if let Some(ref pitch) = self.args.pitch {
173 command.arg("--pitch").arg(pitch);
174 }
175
176 command
177 }
178
179 pub fn execute(self) -> Result<(), Box<dyn std::error::Error>> {
180 self.execute_with_verbose(false)
181 }
182
183 pub fn execute_with_verbose(self, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
184 self.execute_with_retry(verbose, 10)
185 }
186
187 fn execute_with_retry(
188 &self,
189 verbose: bool,
190 max_retries: usize,
191 ) -> Result<(), Box<dyn std::error::Error>> {
192 let mut last_error: Option<Box<dyn std::error::Error>> = None;
193
194 for attempt in 1..=max_retries {
195 let command = self.build_command();
196
197 let output = execute_command_with_timeout(command, 15);
198 let result = match output {
199 Ok(output) => {
200 if verbose {
201 if !output.stdout.is_empty() {
203 print!("{}", String::from_utf8_lossy(&output.stdout));
204 }
205 if !output.stderr.is_empty() {
206 eprint!("{}", String::from_utf8_lossy(&output.stderr));
207 }
208 }
209 if output.status.success() {
210 Ok(())
211 } else {
212 Err("voicepeak command failed".into())
213 }
214 }
215 Err(e) => Err(e.into()),
216 };
217
218 match result {
219 Ok(()) => return Ok(()),
220 Err(e) => {
221 last_error = Some(e);
222 if attempt < max_retries {
223 eprintln!(
224 "VOICEPEAK command failed (attempt {}/{}), retrying in 5 seconds...",
225 attempt, max_retries
226 );
227 kill_all_voicepeak_processes();
229 thread::sleep(Duration::from_secs(5));
230 }
231 }
232 }
233 }
234
235 Err(format!(
236 "VOICEPEAK command failed after {} attempts: {}",
237 max_retries,
238 last_error.unwrap_or_else(|| "unknown error".into())
239 )
240 .into())
241 }
242}
243
244impl Default for VoicepeakCommand {
245 fn default() -> Self {
246 Self::new()
247 }
248}