Views seperation
This commit is contained in:
parent
d4d02bd2d9
commit
303f1a9c66
2 changed files with 334 additions and 219 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
target/
|
||||
*.lock
|
||||
548
src/main.rs
548
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<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,226 +449,20 @@ 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::<String>())
|
||||
} 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
|
||||
|
|
@ -636,3 +471,282 @@ impl eframe::App for TtsUi {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::<String>())
|
||||
} 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue