voirs_cli/commands/interactive/
shell.rs1use super::{
10 commands::CommandProcessor, session::SessionManager, synthesis::SynthesisEngine,
11 InteractiveOptions,
12};
13use crate::config::Config;
14use crate::error::{Result, VoirsCliError};
15use console::{style, Term};
16use dialoguer::{theme::ColorfulTheme, Input};
17use std::collections::HashMap;
18use std::io::Write;
19
20pub struct InteractiveShell {
22 synthesis_engine: SynthesisEngine,
24
25 session_manager: SessionManager,
27
28 command_processor: CommandProcessor,
30
31 history: Vec<String>,
33
34 current_voice: Option<String>,
36 current_speed: f32,
37 current_pitch: f32,
38 current_volume: f32,
39
40 options: InteractiveOptions,
42
43 term: Term,
45
46 available_voices: Vec<String>,
48
49 running: bool,
51}
52
53impl InteractiveShell {
54 pub async fn new(options: InteractiveOptions) -> Result<Self> {
56 let term = Term::stdout();
57
58 let synthesis_engine = SynthesisEngine::new().await?;
60
61 let mut session_manager = SessionManager::new(options.auto_save);
63
64 if let Some(session_path) = &options.load_session {
66 session_manager.load_session(session_path).await?;
67 }
68
69 let available_voices = synthesis_engine.list_voices().await?;
71
72 let command_processor = CommandProcessor::new(available_voices.clone());
74
75 let current_voice = options
77 .voice
78 .clone()
79 .or_else(|| session_manager.get_current_voice().cloned())
80 .or_else(|| available_voices.first().cloned());
81
82 let mut shell = Self {
83 synthesis_engine,
84 session_manager,
85 command_processor,
86 history: Vec::new(),
87 current_voice: current_voice.clone(),
88 current_speed: 1.0,
89 current_pitch: 0.0,
90 current_volume: 1.0,
91 options,
92 term,
93 available_voices,
94 running: true,
95 };
96
97 if let Some(voice) = current_voice {
99 shell.synthesis_engine.set_voice(&voice).await?;
100 }
101
102 Ok(shell)
103 }
104
105 pub async fn run(&mut self) -> Result<()> {
107 self.print_welcome();
108 self.print_help_hint();
109
110 while self.running {
111 match self.read_command().await {
112 Ok(command) => {
113 if let Err(e) = self.process_command(&command).await {
114 self.print_error(&e);
115 }
116 }
117 Err(e) => {
118 if self.should_exit_on_error(&e) {
119 break;
120 }
121 self.print_error(&e);
122 }
123 }
124 }
125
126 self.print_goodbye();
127 Ok(())
128 }
129
130 async fn read_command(&mut self) -> Result<String> {
132 let prompt = self.create_prompt();
133
134 if !console::Term::stdout().is_term() {
136 use std::io::{self, BufRead};
138 let stdin = io::stdin();
139 let mut line = String::new();
140 match stdin.lock().read_line(&mut line) {
141 Ok(0) => {
142 self.running = false;
144 return Ok("quit".to_string());
145 }
146 Ok(_) => {
147 let input = line.trim().to_string();
148 if input == "quit" || input == "exit" {
149 self.running = false;
150 }
151 return Ok(input);
152 }
153 Err(e) => {
154 return Err(VoirsCliError::IoError(format!("Input error: {}", e)));
155 }
156 }
157 }
158
159 let input: String = Input::with_theme(&ColorfulTheme::default())
160 .with_prompt(&prompt)
161 .interact_text()
162 .map_err(|e| VoirsCliError::IoError(format!("Input error: {}", e)))?;
163
164 if !input.trim().is_empty() && self.history.last() != Some(&input) {
166 self.history.push(input.clone());
167
168 if self.history.len() > 1000 {
170 self.history.remove(0);
171 }
172 }
173
174 Ok(input)
175 }
176
177 async fn process_command(&mut self, command: &str) -> Result<()> {
179 let command = command.trim();
180
181 if command.is_empty() {
182 return Ok(());
183 }
184
185 if command.starts_with(':') {
187 return self.handle_shell_command(command).await;
188 }
189
190 self.synthesize_text(command).await?;
192
193 self.session_manager
195 .add_synthesis(command, &self.current_voice);
196
197 Ok(())
198 }
199
200 async fn handle_shell_command(&mut self, command: &str) -> Result<()> {
202 let available_voices = self.command_processor.available_voices().clone();
204 let temp_processor = CommandProcessor::new(available_voices);
205 temp_processor.process_command(self, command).await
206 }
207
208 async fn synthesize_text(&mut self, text: &str) -> Result<()> {
210 let start_time = std::time::Instant::now();
211
212 self.print_status(&format!("Synthesizing: \"{}\"", text));
214
215 let audio_data = self.synthesis_engine.synthesize(text).await?;
217
218 let synthesis_time = start_time.elapsed();
219
220 if !self.options.no_audio {
222 self.synthesis_engine.play_audio(&audio_data).await?;
223 }
224
225 self.print_status(&format!(
227 "✓ Synthesis completed in {:.2}s ({} samples)",
228 synthesis_time.as_secs_f64(),
229 audio_data.len()
230 ));
231
232 Ok(())
233 }
234
235 fn create_prompt(&self) -> String {
237 let voice_part = if let Some(ref voice) = self.current_voice {
238 format!("{}@{}", style("voirs").cyan(), style(voice).green())
239 } else {
240 style("voirs").cyan().to_string()
241 };
242
243 let params =
244 if self.current_speed != 1.0 || self.current_pitch != 0.0 || self.current_volume != 1.0
245 {
246 format!(
247 " [s:{:.1} p:{:.1} v:{:.1}]",
248 self.current_speed, self.current_pitch, self.current_volume
249 )
250 } else {
251 String::new()
252 };
253
254 format!("{}{}> ", voice_part, style(¶ms).dim())
255 }
256
257 fn complete_input(&self, input: &str) -> Vec<String> {
259 let mut suggestions = Vec::new();
260
261 if input.starts_with(':') {
262 let commands = [
264 ":help", ":voice", ":voices", ":speed", ":pitch", ":volume", ":save", ":load",
265 ":history", ":clear", ":status", ":quit", ":exit",
266 ];
267
268 for cmd in &commands {
269 if cmd.starts_with(input) {
270 suggestions.push(cmd.to_string());
271 }
272 }
273 } else if input.contains(" ") && input.starts_with(":voice ") {
274 let voice_prefix = input.strip_prefix(":voice ").unwrap_or("");
276 for voice in &self.available_voices {
277 if voice.starts_with(voice_prefix) {
278 suggestions.push(format!(":voice {}", voice));
279 }
280 }
281 }
282
283 suggestions
284 }
285
286 fn print_welcome(&self) {
288 println!(
289 "{}",
290 style("Welcome to VoiRS Interactive Mode").bold().cyan()
291 );
292 println!("Type text to synthesize, or use :help for commands");
293
294 if let Some(ref voice) = self.current_voice {
295 println!("Current voice: {}", style(voice).green());
296 }
297
298 println!();
299 }
300
301 fn print_help_hint(&self) {
303 println!(
304 "{}",
305 style("Hint: Type ':help' for available commands").dim()
306 );
307 println!();
308 }
309
310 fn print_goodbye(&self) {
312 println!("\\n{}", style("Goodbye! 👋").cyan());
313 }
314
315 fn print_status(&self, message: &str) {
317 println!("{} {}", style("ℹ").blue(), message);
318 }
319
320 fn print_error(&self, error: &VoirsCliError) {
322 eprintln!("{} {}", style("✗").red(), style(error).red());
323 }
324
325 fn should_exit_on_error(&self, _error: &VoirsCliError) -> bool {
327 false
329 }
330
331 pub fn current_voice(&self) -> Option<&str> {
333 self.current_voice.as_deref()
334 }
335
336 pub async fn set_voice(&mut self, voice: String) -> Result<()> {
338 self.synthesis_engine.set_voice(&voice).await?;
339 self.current_voice = Some(voice.clone());
340 self.session_manager.set_current_voice(voice);
341 Ok(())
342 }
343
344 pub fn available_voices(&self) -> &[String] {
346 &self.available_voices
347 }
348
349 pub fn current_params(&self) -> (f32, f32, f32) {
351 (self.current_speed, self.current_pitch, self.current_volume)
352 }
353
354 pub async fn set_params(
356 &mut self,
357 speed: Option<f32>,
358 pitch: Option<f32>,
359 volume: Option<f32>,
360 ) -> Result<()> {
361 if let Some(s) = speed {
362 self.current_speed = s.clamp(0.1, 3.0);
363 self.synthesis_engine.set_speed(self.current_speed).await?;
364 }
365
366 if let Some(p) = pitch {
367 self.current_pitch = p.clamp(-12.0, 12.0);
368 self.synthesis_engine.set_pitch(self.current_pitch).await?;
369 }
370
371 if let Some(v) = volume {
372 self.current_volume = v.clamp(0.0, 2.0);
373 self.synthesis_engine
374 .set_volume(self.current_volume)
375 .await?;
376 }
377
378 Ok(())
379 }
380
381 pub fn session_manager(&mut self) -> &mut SessionManager {
383 &mut self.session_manager
384 }
385
386 pub fn exit(&mut self) {
388 self.running = false;
389 }
390}