voicepeak_cli/
voicepeak.rs

1use 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        // Store child process ID for potential killing
75        {
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            // Kill the process on timeout
88            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                        // Print stdout and stderr in verbose mode
202                        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 any remaining VOICEPEAK processes before retry
228                        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}