Views seperation

This commit is contained in:
jrosh 2025-06-03 14:51:23 +02:00
commit 303f1a9c66
No known key found for this signature in database
GPG key ID: A4D68DCA6C9CCD2D
2 changed files with 334 additions and 219 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
target/ target/
*.lock

View file

@ -62,7 +62,7 @@ fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_inner_size([700.0, 400.0]) .with_inner_size([400.0, 200.0])
.with_resizable(true) .with_resizable(true)
.with_decorations(false), .with_decorations(false),
..Default::default() ..Default::default()
@ -162,6 +162,14 @@ fn smart_chunk_text(text: &str, config: &ChunkingConfig) -> Vec<String> {
chunks chunks
} }
#[derive(Debug, Clone, PartialEq)]
enum CurrentView {
Main,
TextInput,
Configuration,
}
struct TtsUi { struct TtsUi {
original_text: String, original_text: String,
cleaned_text: String, cleaned_text: String,
@ -176,6 +184,9 @@ struct TtsUi {
should_auto_process: bool, should_auto_process: bool,
frames_since_load: u32, frames_since_load: u32,
status_receiver: Option<mpsc::Receiver<ProcessingStatus>>, status_receiver: Option<mpsc::Receiver<ProcessingStatus>>,
current_view: CurrentView,
viewport_expanded: bool,
original_size: egui::Vec2,
} }
impl TtsUi { impl TtsUi {
@ -197,9 +208,31 @@ impl TtsUi {
should_auto_process: has_text, should_auto_process: has_text,
frames_since_load: 0, frames_since_load: 0,
status_receiver: None, 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) { fn start_tts_processing(&mut self) {
if self.is_processing || self.cleaned_text.trim().is_empty() { if self.is_processing || self.cleaned_text.trim().is_empty() {
return; return;
@ -265,6 +298,9 @@ impl TtsUi {
while let Ok(audio_file) = audio_receiver.recv() { while let Ok(audio_file) = audio_receiver.recv() {
let _ = player_sender.send(ProcessingStatus::PlayingChunk(chunk_num, total_chunks)); let _ = player_sender.send(ProcessingStatus::PlayingChunk(chunk_num, total_chunks));
Self::play_audio_sync(&audio_file); Self::play_audio_sync(&audio_file);
if chunk_num < total_chunks {
let _ = player_sender.send(ProcessingStatus::Idle);
}
chunk_num += 1; chunk_num += 1;
// If this was the last chunk, we're done // If this was the last chunk, we're done
@ -387,10 +423,15 @@ impl eframe::App for TtsUi {
self.currently_playing_chunk = None; self.currently_playing_chunk = None;
self.is_processing = false; self.is_processing = false;
} }
ProcessingStatus::Idle => {
// Reset currently playing chunk when between chunks
self.currently_playing_chunk = None;
}
_ => {} _ => {}
} }
self.processing_status = new_status; self.processing_status = new_status;
} }
ctx.request_repaint();
} }
if !self.window_loaded { if !self.window_loaded {
@ -408,57 +449,74 @@ impl eframe::App for TtsUi {
} }
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
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)) {
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
if !self.window_loaded {
ctx.request_repaint();
}
}
}
impl TtsUi {
fn show_main_view(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.auto_shrink([false; 2]) .auto_shrink([false; 2])
.show(ui, |ui| { .show(ui, |ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
// Main control buttons
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Status:"); if ui.button("📝").clicked() {
if self.is_processing { self.switch_to_view(CurrentView::TextInput, ctx);
ui.spinner(); }
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.label(&self.status);
}); });
ui.separator(); ui.separator();
ui.add_space(10.0); 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 // Show the chunk that is currently being played
if let Some(playing_chunk) = self.currently_playing_chunk { if let Some(playing_chunk) = self.currently_playing_chunk {
if let Some(playing_text) = self.chunks.get(playing_chunk - 1) { if let Some(playing_text) = self.chunks.get(playing_chunk - 1) {
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.max_height(120.0) .max_height(80.0)
.show(ui, |ui| { .show(ui, |ui| {
ui.add( ui.label(playing_text);
egui::TextEdit::multiline(&mut playing_text.as_str())
.desired_width(f32::INFINITY)
.desired_rows(1)
.interactive(false)
);
}); });
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 { } else {
match &self.processing_status { match &self.processing_status {
@ -472,32 +530,33 @@ impl eframe::App for TtsUi {
ui.label(format!("⏳ Processing chunk {}/{}... Audio will start soon.", 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() { if !self.is_processing && self.chunks.is_empty() {
egui::ScrollArea::vertical() ui.label("📝 Click 'Text Input' to enter text, then process with Kokoro");
.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 { } else {
ui.label("⏳ Waiting for audio playback to begin..."); ui.label("⏳ Waiting for audio playback to begin...");
} }
} }
} }
} }
});
});
}
ui.add_space(10.0); 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.separator();
ui.add_space(5.0); ui.heading("📝 Text Input");
});
ui.separator();
ui.add_space(10.0);
// Collapsible text input section (moved above current audio) // Text info
ui.collapsing("📝 Text Input", |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Text to process:"); ui.label("Text to process:");
ui.label(format!("({} characters)", self.cleaned_text.len())); ui.label(format!("({} characters)", self.cleaned_text.len()));
@ -506,27 +565,31 @@ impl eframe::App for TtsUi {
} }
}); });
ui.add_space(10.0);
// Large text input area
ui.label("Text Content:");
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.max_height(150.0) .max_height(300.0)
.show(ui, |ui| { .show(ui, |ui| {
ui.add( ui.add(
egui::TextEdit::multiline(&mut self.cleaned_text) egui::TextEdit::multiline(&mut self.cleaned_text)
.desired_width(f32::INFINITY) .desired_width(f32::INFINITY)
.desired_rows(8) .desired_rows(20)
.interactive(!self.is_processing) .interactive(!self.is_processing)
); );
}); });
// Show all chunks if available // Show all chunks if available
if !self.chunks.is_empty() { if !self.chunks.is_empty() {
ui.add_space(5.0); ui.add_space(15.0);
ui.label("📄 All Chunks:"); ui.label("All Chunks:");
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.max_height(100.0) .max_height(200.0)
.show(ui, |ui| { .show(ui, |ui| {
for (i, chunk) in self.chunks.iter().enumerate() { for (i, chunk) in self.chunks.iter().enumerate() {
let chunk_preview = if chunk.len() > 80 { let chunk_preview = if chunk.len() > 120 {
format!("{}...", &chunk.chars().take(80).collect::<String>()) format!("{}...", &chunk.chars().take(120).collect::<String>())
} else { } else {
chunk.clone() chunk.clone()
}; };
@ -546,93 +609,144 @@ impl eframe::App for TtsUi {
} }
}); });
} }
});
ui.add_space(15.0);
ui.separator();
ui.add_space(10.0); ui.add_space(10.0);
// Controls // Action buttons
ui.horizontal(|ui| { 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 { if ui.button("🎵 Process with Kokoro").clicked() && !self.is_processing {
self.start_tts_processing(); self.start_tts_processing();
self.switch_to_view(CurrentView::Main, ctx);
} }
ui.separator(); ui.separator();
if ui.button("❌ Exit").clicked() { if ui.button("💾 Save & Return").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); self.switch_to_view(CurrentView::Main, ctx);
} }
}); });
});
}
ui.add_space(10.0); 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.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.add_space(5.0);
// Configuration panel
ui.collapsing("⚙ Configuration", |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Speed:"); ui.label("Speed:");
ui.add(egui::Slider::new(&mut self.config.speed, 0.5..=2.0).step_by(0.1)); 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.horizontal(|ui| {
ui.label("Voice Style:"); ui.label("Voice Style:");
ui.text_edit_singleline(&mut self.config.voice_style); ui.text_edit_singleline(&mut self.config.voice_style);
}); });
});
ui.separator(); ui.add_space(15.0);
ui.label("Chunking Settings:");
// Chunking Settings
ui.group(|ui| {
ui.label("📄 Chunking Settings");
ui.add_space(5.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Min chunk size:"); ui.label("Min chunk size:");
ui.add(egui::DragValue::new(&mut self.config.chunking.min_chunk_size).clamp_range(20..=500)); ui.add(egui::DragValue::new(&mut self.config.chunking.min_chunk_size).clamp_range(20..=500));
ui.label("characters");
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Max chunk size:"); ui.label("Max chunk size:");
ui.add(egui::DragValue::new(&mut self.config.chunking.max_chunk_size).clamp_range(50..=1000)); ui.add(egui::DragValue::new(&mut self.config.chunking.max_chunk_size).clamp_range(50..=1000));
ui.label("characters");
}); });
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Min sentences:"); ui.label("Min sentences:");
ui.add(egui::DragValue::new(&mut self.config.chunking.min_sentences).clamp_range(1..=10)); ui.add(egui::DragValue::new(&mut self.config.chunking.min_sentences).clamp_range(1..=10));
ui.label("per chunk");
});
}); });
ui.separator(); ui.add_space(15.0);
ui.label("Paths:");
ui.horizontal(|ui| { // Path Settings
ui.label("Executable:"); 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.text_edit_singleline(&mut self.config.exec_path);
});
ui.horizontal(|ui| { ui.add_space(8.0);
ui.label("Model Path:"); ui.label("Model Path:");
ui.text_edit_singleline(&mut self.config.model_path); ui.text_edit_singleline(&mut self.config.model_path);
});
ui.horizontal(|ui| { ui.add_space(8.0);
ui.label("Voice Data:");
ui.label("Voice Data Path:");
ui.text_edit_singleline(&mut self.config.voice_data); ui.text_edit_singleline(&mut self.config.voice_data);
}); });
}); });
ui.add_space(10.0); ui.add_space(20.0);
});
});
ui.separator(); ui.separator();
ui.add_space(5.0); ui.add_space(10.0);
});
});
});
// Close on Escape // Action buttons
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { ui.horizontal(|ui| {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); if ui.button("✅ Save & Return").clicked() {
self.switch_to_view(CurrentView::Main, ctx);
} }
// Request repaint to keep checking window load status ui.separator();
if !self.window_loaded {
ctx.request_repaint(); 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();
}
});
});
});
} }
} }