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/
*.lock

View file

@ -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<String> {
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<mpsc::Receiver<ProcessingStatus>>,
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 {
@ -408,57 +449,74 @@ impl eframe::App for TtsUi {
}
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()
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical(|ui| {
// Main control buttons
ui.horizontal(|ui| {
ui.label("Status:");
if self.is_processing {
ui.spinner();
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.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)
.max_height(80.0)
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut playing_text.as_str())
.desired_width(f32::INFINITY)
.desired_rows(1)
.interactive(false)
);
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 {
@ -472,32 +530,33 @@ impl eframe::App for TtsUi {
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)
);
});
ui.label("📝 Click 'Text Input' to enter text, then process with Kokoro");
} else {
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.add_space(5.0);
ui.heading("📝 Text Input");
});
ui.separator();
ui.add_space(10.0);
// Collapsible text input section (moved above current audio)
ui.collapsing("📝 Text Input", |ui| {
// Text info
ui.horizontal(|ui| {
ui.label("Text to process:");
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()
.max_height(150.0)
.max_height(300.0)
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut self.cleaned_text)
.desired_width(f32::INFINITY)
.desired_rows(8)
.desired_rows(20)
.interactive(!self.is_processing)
);
});
// Show all chunks if available
if !self.chunks.is_empty() {
ui.add_space(5.0);
ui.label("📄 All Chunks:");
ui.add_space(15.0);
ui.label("All Chunks:");
egui::ScrollArea::vertical()
.max_height(100.0)
.max_height(200.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::<String>())
let chunk_preview = if chunk.len() > 120 {
format!("{}...", &chunk.chars().take(120).collect::<String>())
} else {
chunk.clone()
};
@ -546,93 +609,144 @@ impl eframe::App for TtsUi {
}
});
}
});
ui.add_space(15.0);
ui.separator();
ui.add_space(10.0);
// Controls
// 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("❌ Exit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
if ui.button("💾 Save & Return").clicked() {
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.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);
// 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.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.separator();
ui.label("Chunking Settings:");
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.separator();
ui.label("Paths:");
ui.add_space(15.0);
ui.horizontal(|ui| {
ui.label("Executable:");
// 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.horizontal(|ui| {
ui.add_space(8.0);
ui.label("Model Path:");
ui.text_edit_singleline(&mut self.config.model_path);
});
ui.horizontal(|ui| {
ui.label("Voice Data:");
ui.add_space(8.0);
ui.label("Voice Data Path:");
ui.text_edit_singleline(&mut self.config.voice_data);
});
});
ui.add_space(10.0);
ui.add_space(20.0);
});
});
ui.separator();
ui.add_space(5.0);
});
});
});
ui.add_space(10.0);
// Close on Escape
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
// Action buttons
ui.horizontal(|ui| {
if ui.button("✅ Save & Return").clicked() {
self.switch_to_view(CurrentView::Main, ctx);
}
// Request repaint to keep checking window load status
if !self.window_loaded {
ctx.request_repaint();
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();
}
});
});
});
}
}