diff --git a/.gitignore b/.gitignore index 9f97022..b1646c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -target/ \ No newline at end of file +target/ +*.lock \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fa86d03..7c23e15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,7 +62,7 @@ fn main() -> Result<(), eframe::Error> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size([700.0, 400.0]) + .with_inner_size([400.0, 200.0]) .with_resizable(true) .with_decorations(false), ..Default::default() @@ -162,6 +162,14 @@ fn smart_chunk_text(text: &str, config: &ChunkingConfig) -> Vec { chunks } + +#[derive(Debug, Clone, PartialEq)] +enum CurrentView { + Main, + TextInput, + Configuration, +} + struct TtsUi { original_text: String, cleaned_text: String, @@ -176,6 +184,9 @@ struct TtsUi { should_auto_process: bool, frames_since_load: u32, status_receiver: Option>, + current_view: CurrentView, + viewport_expanded: bool, + original_size: egui::Vec2, } impl TtsUi { @@ -197,9 +208,31 @@ impl TtsUi { should_auto_process: has_text, frames_since_load: 0, status_receiver: None, + current_view: CurrentView::Main, + viewport_expanded: false, + original_size: egui::Vec2::new(500.0, 200.0), } } + fn switch_to_view(&mut self, view: CurrentView, ctx: &egui::Context) { + if self.current_view == view { + return; + } + + self.current_view = view.clone(); + + // Resize viewport based on view + let new_size = match view { + CurrentView::Main => self.original_size, + CurrentView::TextInput => egui::Vec2::new(800.0, 600.0), + CurrentView::Configuration => egui::Vec2::new(700.0, 650.0), + }; + + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(new_size)); + self.viewport_expanded = view != CurrentView::Main; + } + + fn start_tts_processing(&mut self) { if self.is_processing || self.cleaned_text.trim().is_empty() { return; @@ -265,6 +298,9 @@ impl TtsUi { while let Ok(audio_file) = audio_receiver.recv() { let _ = player_sender.send(ProcessingStatus::PlayingChunk(chunk_num, total_chunks)); Self::play_audio_sync(&audio_file); + if chunk_num < total_chunks { + let _ = player_sender.send(ProcessingStatus::Idle); + } chunk_num += 1; // If this was the last chunk, we're done @@ -387,10 +423,15 @@ impl eframe::App for TtsUi { self.currently_playing_chunk = None; self.is_processing = false; } + ProcessingStatus::Idle => { + // Reset currently playing chunk when between chunks + self.currently_playing_chunk = None; + } _ => {} } self.processing_status = new_status; } + ctx.request_repaint(); } if !self.window_loaded { @@ -406,228 +447,22 @@ impl eframe::App for TtsUi { } } } - + egui::CentralPanel::default().show(ctx, |ui| { - egui::ScrollArea::vertical() - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.label("Status:"); - if self.is_processing { - ui.spinner(); - } - ui.label(&self.status); - }); - ui.separator(); - ui.add_space(10.0); - - - // Current audio display - ui.horizontal(|ui| { - ui.label("🎵 Currently Playing:"); - if let Some(playing_chunk) = self.currently_playing_chunk { - ui.label(format!("Chunk {}/{}", playing_chunk, self.chunks.len())); - } else { - match &self.processing_status { - ProcessingStatus::Completed => { - ui.label("Playback completed"); - } - ProcessingStatus::Error(_) => { - ui.label("Error occurred"); - } - ProcessingStatus::ProcessingChunk(_, _) => { - ui.label("Preparing audio..."); - } - _ => { - ui.label("Ready to start"); - } - } - } - }); - - // Show the chunk that is currently being played - if let Some(playing_chunk) = self.currently_playing_chunk { - if let Some(playing_text) = self.chunks.get(playing_chunk - 1) { - egui::ScrollArea::vertical() - .max_height(120.0) - .show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(&mut playing_text.as_str()) - .desired_width(f32::INFINITY) - .desired_rows(1) - .interactive(false) - ); - }); - } - } else { - match &self.processing_status { - ProcessingStatus::Completed => { - ui.label("🎉 All audio chunks have been played successfully!"); - } - ProcessingStatus::Error(err) => { - ui.colored_label(egui::Color32::RED, format!("❌ Error: {}", err)); - } - ProcessingStatus::ProcessingChunk(current, total) => { - ui.label(format!("⏳ Processing chunk {}/{}... Audio will start soon.", current, total)); - } - _ => { - // When not playing anything, show the input text for editing - if !self.is_processing && self.chunks.is_empty() { - egui::ScrollArea::vertical() - .max_height(120.0) - .show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(&mut self.cleaned_text) - .desired_width(f32::INFINITY) - .desired_rows(5) - .interactive(!self.is_processing) - ); - }); - } else { - ui.label("⏳ Waiting for audio playback to begin..."); - } - } - } - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(5.0); - - - // Collapsible text input section (moved above current audio) - ui.collapsing("📝 Text Input", |ui| { - ui.horizontal(|ui| { - ui.label("Text to process:"); - ui.label(format!("({} characters)", self.cleaned_text.len())); - if !self.chunks.is_empty() { - ui.label(format!("- {} chunks", self.chunks.len())); - } - }); - - egui::ScrollArea::vertical() - .max_height(150.0) - .show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(&mut self.cleaned_text) - .desired_width(f32::INFINITY) - .desired_rows(8) - .interactive(!self.is_processing) - ); - }); - - // Show all chunks if available - if !self.chunks.is_empty() { - ui.add_space(5.0); - ui.label("📄 All Chunks:"); - egui::ScrollArea::vertical() - .max_height(100.0) - .show(ui, |ui| { - for (i, chunk) in self.chunks.iter().enumerate() { - let chunk_preview = if chunk.len() > 80 { - format!("{}...", &chunk.chars().take(80).collect::()) - } else { - chunk.clone() - }; - - // Highlight current chunk being processed or played - let is_current = match &self.processing_status { - ProcessingStatus::ProcessingChunk(current, _) => *current == i + 1, - ProcessingStatus::PlayingChunk(current, _) => *current == i + 1, - _ => false, - }; - - if is_current { - ui.colored_label(egui::Color32::GREEN, format!("▶ {}: {}", i + 1, chunk_preview)); - } else { - ui.label(format!("{}: {}", i + 1, chunk_preview)); - } - } - }); - } - }); - - ui.add_space(10.0); - - // Controls - ui.horizontal(|ui| { - if ui.button("🎵 Process with Kokoro").clicked() && !self.is_processing { - self.start_tts_processing(); - } - - ui.separator(); - - if ui.button("❌ Exit").clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); - } - }); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(5.0); - - - - // Configuration panel - ui.collapsing("⚙ Configuration", |ui| { - ui.horizontal(|ui| { - ui.label("Speed:"); - ui.add(egui::Slider::new(&mut self.config.speed, 0.5..=2.0).step_by(0.1)); - }); - - ui.horizontal(|ui| { - ui.label("Voice Style:"); - ui.text_edit_singleline(&mut self.config.voice_style); - }); - - ui.separator(); - ui.label("Chunking Settings:"); - - ui.horizontal(|ui| { - ui.label("Min chunk size:"); - ui.add(egui::DragValue::new(&mut self.config.chunking.min_chunk_size).clamp_range(20..=500)); - }); - - ui.horizontal(|ui| { - ui.label("Max chunk size:"); - ui.add(egui::DragValue::new(&mut self.config.chunking.max_chunk_size).clamp_range(50..=1000)); - }); - - ui.horizontal(|ui| { - ui.label("Min sentences:"); - ui.add(egui::DragValue::new(&mut self.config.chunking.min_sentences).clamp_range(1..=10)); - }); - - ui.separator(); - ui.label("Paths:"); - - ui.horizontal(|ui| { - ui.label("Executable:"); - ui.text_edit_singleline(&mut self.config.exec_path); - }); - - ui.horizontal(|ui| { - ui.label("Model Path:"); - ui.text_edit_singleline(&mut self.config.model_path); - }); - - ui.horizontal(|ui| { - ui.label("Voice Data:"); - ui.text_edit_singleline(&mut self.config.voice_data); - }); - }); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(5.0); - }); - }); + match self.current_view { + CurrentView::Main => self.show_main_view(ui, ctx), + CurrentView::TextInput => self.show_text_input_view(ui, ctx), + CurrentView::Configuration => self.show_configuration_view(ui, ctx), + } }); // Close on Escape if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { - ctx.send_viewport_cmd(egui::ViewportCommand::Close); + if self.current_view != CurrentView::Main { + self.switch_to_view(CurrentView::Main, ctx); + } else { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } } // Request repaint to keep checking window load status @@ -635,4 +470,283 @@ impl eframe::App for TtsUi { ctx.request_repaint(); } } +} + +impl TtsUi { + fn show_main_view(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical(|ui| { + + // Main control buttons + ui.horizontal(|ui| { + if ui.button("📝").clicked() { + self.switch_to_view(CurrentView::TextInput, ctx); + } + + ui.separator(); + + if ui.button("⚙").clicked() { + self.switch_to_view(CurrentView::Configuration, ctx); + } + + ui.separator(); + + if ui.button("❌ Exit").clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + }); + ui.separator(); + ui.add_space(10.0); + + // Show the chunk that is currently being played + if let Some(playing_chunk) = self.currently_playing_chunk { + if let Some(playing_text) = self.chunks.get(playing_chunk - 1) { + + egui::ScrollArea::vertical() + .max_height(80.0) + .show(ui, |ui| { + ui.label(playing_text); + }); + + ui.add_space(10.0); + + if let ProcessingStatus::PlayingChunk(current, total) = &self.processing_status { + ui.label(format!("🎵 {}/{}", current, total)); + ui.add_space(5.0); + } + + } + } else { + match &self.processing_status { + ProcessingStatus::Completed => { + ui.label("🎉 All audio chunks have been played successfully!"); + } + ProcessingStatus::Error(err) => { + ui.colored_label(egui::Color32::RED, format!("❌ Error: {}", err)); + } + ProcessingStatus::ProcessingChunk(current, total) => { + ui.label(format!("⏳ Processing chunk {}/{}... Audio will start soon.", current, total)); + } + _ => { + if !self.is_processing && self.chunks.is_empty() { + ui.label("📝 Click 'Text Input' to enter text, then process with Kokoro"); + } else { + ui.label("⏳ Waiting for audio playback to begin..."); + } + } + } + } + }); + }); + } + + fn show_text_input_view(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + ui.vertical(|ui| { + // Header with back button + ui.horizontal(|ui| { + if ui.button("< Back").clicked() { + self.switch_to_view(CurrentView::Main, ctx); + } + ui.separator(); + ui.heading("📝 Text Input"); + }); + + ui.separator(); + ui.add_space(10.0); + + // Text info + ui.horizontal(|ui| { + ui.label("Text to process:"); + ui.label(format!("({} characters)", self.cleaned_text.len())); + if !self.chunks.is_empty() { + ui.label(format!("- {} chunks", self.chunks.len())); + } + }); + + ui.add_space(10.0); + + // Large text input area + ui.label("Text Content:"); + egui::ScrollArea::vertical() + .max_height(300.0) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut self.cleaned_text) + .desired_width(f32::INFINITY) + .desired_rows(20) + .interactive(!self.is_processing) + ); + }); + + // Show all chunks if available + if !self.chunks.is_empty() { + ui.add_space(15.0); + ui.label("All Chunks:"); + egui::ScrollArea::vertical() + .max_height(200.0) + .show(ui, |ui| { + for (i, chunk) in self.chunks.iter().enumerate() { + let chunk_preview = if chunk.len() > 120 { + format!("{}...", &chunk.chars().take(120).collect::()) + } else { + chunk.clone() + }; + + // Highlight current chunk being processed or played + let is_current = match &self.processing_status { + ProcessingStatus::ProcessingChunk(current, _) => *current == i + 1, + ProcessingStatus::PlayingChunk(current, _) => *current == i + 1, + _ => false, + }; + + if is_current { + ui.colored_label(egui::Color32::GREEN, format!("▶ {}: {}", i + 1, chunk_preview)); + } else { + ui.label(format!("{}: {}", i + 1, chunk_preview)); + } + } + }); + } + + ui.add_space(15.0); + ui.separator(); + ui.add_space(10.0); + + // Action buttons + ui.horizontal(|ui| { + if ui.button("🔄 Re-chunk Text").clicked() { + self.chunks = smart_chunk_text(&self.cleaned_text, &self.config.chunking); + } + + ui.separator(); + + if ui.button("🎵 Process with Kokoro").clicked() && !self.is_processing { + self.start_tts_processing(); + self.switch_to_view(CurrentView::Main, ctx); + } + + ui.separator(); + + if ui.button("💾 Save & Return").clicked() { + self.switch_to_view(CurrentView::Main, ctx); + } + }); + }); + } + + fn show_configuration_view(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + ui.vertical(|ui| { + // Header with back button + ui.horizontal(|ui| { + if ui.button("< Back").clicked() { + self.switch_to_view(CurrentView::Main, ctx); + } + ui.separator(); + ui.heading("⚙ Configuration"); + }); + + ui.separator(); + ui.add_space(10.0); + + egui::ScrollArea::vertical() + .show(ui, |ui| { + ui.vertical(|ui| { + // Audio Settings + ui.group(|ui| { + ui.label("🎵 Audio Settings"); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Speed:"); + ui.add(egui::Slider::new(&mut self.config.speed, 0.5..=2.0).step_by(0.1)); + ui.label(format!("{:.1}x", self.config.speed)); + }); + + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Voice Style:"); + ui.text_edit_singleline(&mut self.config.voice_style); + }); + }); + + ui.add_space(15.0); + + // Chunking Settings + ui.group(|ui| { + ui.label("📄 Chunking Settings"); + ui.add_space(5.0); + + ui.horizontal(|ui| { + ui.label("Min chunk size:"); + ui.add(egui::DragValue::new(&mut self.config.chunking.min_chunk_size).clamp_range(20..=500)); + ui.label("characters"); + }); + + ui.horizontal(|ui| { + ui.label("Max chunk size:"); + ui.add(egui::DragValue::new(&mut self.config.chunking.max_chunk_size).clamp_range(50..=1000)); + ui.label("characters"); + }); + + ui.horizontal(|ui| { + ui.label("Min sentences:"); + ui.add(egui::DragValue::new(&mut self.config.chunking.min_sentences).clamp_range(1..=10)); + ui.label("per chunk"); + }); + }); + + ui.add_space(15.0); + + // Path Settings + ui.group(|ui| { + ui.label("📁 File Paths"); + ui.add_space(5.0); + + ui.vertical(|ui| { + ui.label("Kokoro Executable:"); + ui.text_edit_singleline(&mut self.config.exec_path); + + ui.add_space(8.0); + + ui.label("Model Path:"); + ui.text_edit_singleline(&mut self.config.model_path); + + ui.add_space(8.0); + + ui.label("Voice Data Path:"); + ui.text_edit_singleline(&mut self.config.voice_data); + }); + }); + + ui.add_space(20.0); + }); + }); + + ui.separator(); + ui.add_space(10.0); + + // Action buttons + ui.horizontal(|ui| { + if ui.button("✅ Save & Return").clicked() { + self.switch_to_view(CurrentView::Main, ctx); + } + + ui.separator(); + + if ui.button("❌ Cancel").clicked() { + // TODO: Restore previous config values + self.switch_to_view(CurrentView::Main, ctx); + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("🔄 Reset to Defaults").clicked() { + self.config = KokoroConfig::default(); + } + }); + }); + }); + } } \ No newline at end of file