diff --git a/README.md b/README.md
index d1ca421..39576e0 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,7 @@
# Simple TTS UI
My tts ui program for unix
-I made this program as an alternative to my bash script for text chunking and sequential streaming
-
-Screenshot:
-
+Made this program as an alternative to my bash script for text chunking and sequential streaming
## Installation
@@ -12,7 +9,7 @@ Dependencies: hexgrad/Kokoro or lucasjinreal/Kokoros and a player aplay/paplay/p
Build dependencies: eframe, egui, atty, tempfile
-Set path for TTS: (these settings can be changed after build)
+Set path for TTS:
```rust
impl Default for KokoroConfig {
@@ -32,4 +29,10 @@ impl Default for KokoroConfig {
Build this app with `cargo build --release`
-Then run `echo "Hello, world" | ./target/release/ttsui`
\ No newline at end of file
+Then run `echo "Hello, world" | ./target/release/ttsui`
+
+Screenshots:
+
+
+
+
\ No newline at end of file
diff --git a/assets/screenshot1.png b/assets/screenshot1.png
new file mode 100644
index 0000000..8a2ad3f
Binary files /dev/null and b/assets/screenshot1.png differ
diff --git a/assets/screenshot2.png b/assets/screenshot2.png
new file mode 100644
index 0000000..caf23de
Binary files /dev/null and b/assets/screenshot2.png differ
diff --git a/screenshot.png b/screenshot.png
deleted file mode 100644
index 5243b41..0000000
Binary files a/screenshot.png and /dev/null differ
diff --git a/src/main.rs b/src/main.rs
index 7c23e15..2283f4f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,10 +1,11 @@
use eframe::egui;
use std::io::{self, Read};
-use std::process::{Command, Stdio};
+use std::process::{Command, Stdio, Child};
use std::path::PathBuf;
use tempfile::TempDir;
use std::thread;
use std::sync::mpsc;
+use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
struct ChunkingConfig {
@@ -57,12 +58,20 @@ enum ProcessingStatus {
Error(String),
}
+// Audio control for pause/resume
+#[derive(Debug, Clone)]
+enum AudioControl {
+ Play,
+ Pause,
+ Stop,
+}
+
fn main() -> Result<(), eframe::Error> {
let input_text = get_piped_text();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
- .with_inner_size([400.0, 200.0])
+ .with_inner_size([400.0, 100.0])
.with_resizable(true)
.with_decorations(false),
..Default::default()
@@ -187,6 +196,9 @@ struct TtsUi {
current_view: CurrentView,
viewport_expanded: bool,
original_size: egui::Vec2,
+ // Audio control
+ audio_control_sender: Option>,
+ is_paused: bool,
}
impl TtsUi {
@@ -211,6 +223,8 @@ impl TtsUi {
current_view: CurrentView::Main,
viewport_expanded: false,
original_size: egui::Vec2::new(500.0, 200.0),
+ audio_control_sender: None,
+ is_paused: false,
}
}
@@ -232,6 +246,19 @@ impl TtsUi {
self.viewport_expanded = view != CurrentView::Main;
}
+ fn toggle_pause_play(&mut self) {
+ if let Some(ref sender) = self.audio_control_sender {
+ let control = if self.is_paused {
+ AudioControl::Play
+ } else {
+ AudioControl::Pause
+ };
+
+ if sender.send(control).is_ok() {
+ self.is_paused = !self.is_paused;
+ }
+ }
+ }
fn start_tts_processing(&mut self) {
if self.is_processing || self.cleaned_text.trim().is_empty() {
@@ -269,9 +296,13 @@ impl TtsUi {
let (sender, receiver) = mpsc::channel();
self.status_receiver = Some(receiver);
+ // Create audio control channel
+ let (audio_control_sender, audio_control_receiver) = mpsc::channel();
+ self.audio_control_sender = Some(audio_control_sender);
+
// Process chunks in background thread
thread::spawn(move || {
- Self::process_chunks_background(temp_path, chunks, config, sender);
+ Self::process_chunks_background(temp_path, chunks, config, sender, audio_control_receiver);
});
self.status = "Starting chunk processing...".to_string();
@@ -282,7 +313,8 @@ impl TtsUi {
temp_path: PathBuf,
chunks: Vec,
config: KokoroConfig,
- sender: mpsc::Sender
+ sender: mpsc::Sender,
+ audio_control_receiver: mpsc::Receiver,
) {
let total_chunks = chunks.len();
@@ -291,13 +323,25 @@ impl TtsUi {
// Spawn audio player thread
let player_sender = sender.clone();
+ let audio_control = Arc::new(Mutex::new(AudioControl::Play));
+ let audio_control_clone = audio_control.clone();
+
+ // Thread to handle audio control messages
+ thread::spawn(move || {
+ while let Ok(control) = audio_control_receiver.recv() {
+ if let Ok(mut audio_ctrl) = audio_control_clone.lock() {
+ *audio_ctrl = control;
+ }
+ }
+ });
+
thread::spawn(move || {
let mut chunk_num = 1;
// Play audio files as they become available
while let Ok(audio_file) = audio_receiver.recv() {
let _ = player_sender.send(ProcessingStatus::PlayingChunk(chunk_num, total_chunks));
- Self::play_audio_sync(&audio_file);
+ Self::play_audio_sync(&audio_file, audio_control.clone());
if chunk_num < total_chunks {
let _ = player_sender.send(ProcessingStatus::Idle);
}
@@ -367,10 +411,13 @@ impl TtsUi {
println!("✅ All chunks processed!");
}
- fn play_audio_sync(audio_file: &PathBuf) {
+ fn play_audio_sync(audio_file: &PathBuf, audio_control: Arc>) {
let players = ["aplay", "paplay", "play", "ffplay", "mpg123"];
for player in &players {
+ let mut child_opt: Option = None;
+ let mut is_paused = false;
+
let result = Command::new(player)
.arg(audio_file)
.stdout(Stdio::null())
@@ -378,19 +425,86 @@ impl TtsUi {
.spawn();
match result {
- Ok(mut child) => {
+ Ok(child) => {
println!("Playing audio with {}...", player);
- match child.wait() {
- Ok(exit_status) => {
- if exit_status.success() {
- println!("✅ Chunk played successfully!");
- return;
+ child_opt = Some(child);
+
+ // Handle pause/play with polling
+ loop {
+ // Check audio control state
+ if let Ok(control) = audio_control.lock() {
+ match *control {
+ AudioControl::Pause => {
+ if !is_paused {
+ // Kill the current process
+ if let Some(ref mut c) = child_opt {
+ let _ = c.kill();
+ let _ = c.wait(); // Wait for process to actually die
+ child_opt = None;
+ }
+ is_paused = true;
+ println!("Audio paused");
+ }
+ }
+ AudioControl::Play => {
+ if is_paused && child_opt.is_none() {
+ // Restart playback from beginning
+ match Command::new(player)
+ .arg(audio_file)
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .spawn() {
+ Ok(new_child) => {
+ child_opt = Some(new_child);
+ is_paused = false;
+ println!("Audio resumed");
+ }
+ Err(e) => {
+ eprintln!("Failed to restart audio: {}", e);
+ return;
+ }
+ }
+ }
+ }
+ AudioControl::Stop => {
+ if let Some(ref mut c) = child_opt {
+ let _ = c.kill();
+ let _ = c.wait();
+ }
+ return;
+ }
}
}
- Err(e) => {
- eprintln!("Error during playback: {}", e);
+
+ // Check if process finished (only if we have an active process)
+ if let Some(ref mut c) = child_opt {
+ match c.try_wait() {
+ Ok(Some(exit_status)) => {
+ if exit_status.success() && !is_paused {
+ println!("✅ Chunk played successfully!");
+ return;
+ } else if !exit_status.success() {
+ eprintln!("Audio process failed");
+ break;
+ }
+ // If paused, the process died intentionally, continue loop
+ }
+ Ok(None) => {
+ // Still running, continue
+ }
+ Err(_) => {
+ eprintln!("Error checking process status");
+ break;
+ }
+ }
}
+
+ // Small sleep to prevent busy waiting
+ thread::sleep(std::time::Duration::from_millis(50));
}
+
+ // If we got here and found a working player, don't try others
+ return;
}
Err(_) => continue,
}
@@ -417,15 +531,20 @@ impl eframe::App for TtsUi {
self.status = "✅ All chunks completed successfully!".to_string();
self.currently_playing_chunk = None;
self.is_processing = false;
+ self.audio_control_sender = None;
+ self.is_paused = false;
}
ProcessingStatus::Error(err) => {
self.status = format!("❌ Error: {}", err);
self.currently_playing_chunk = None;
self.is_processing = false;
+ self.audio_control_sender = None;
+ self.is_paused = false;
}
ProcessingStatus::Idle => {
// Reset currently playing chunk when between chunks
self.currently_playing_chunk = None;
+ self.is_paused = false;
}
_ => {}
}
@@ -478,7 +597,6 @@ impl TtsUi {
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.vertical(|ui| {
-
// Main control buttons
ui.horizontal(|ui| {
if ui.button("📝").clicked() {
@@ -493,9 +611,19 @@ impl TtsUi {
ui.separator();
- if ui.button("❌ Exit").clicked() {
- ctx.send_viewport_cmd(egui::ViewportCommand::Close);
+ if self.currently_playing_chunk.is_some() && self.audio_control_sender.is_some() {
+ let button_text = if self.is_paused { "▶" } else { "⏸" };
+ if ui.button(button_text).clicked() {
+ self.toggle_pause_play();
+ }
+ ui.separator();
}
+
+ ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+ if ui.button("❌").clicked() {
+ ctx.send_viewport_cmd(egui::ViewportCommand::Close);
+ }
+ });
});
ui.separator();
ui.add_space(10.0);
@@ -513,7 +641,12 @@ impl TtsUi {
ui.add_space(10.0);
if let ProcessingStatus::PlayingChunk(current, total) = &self.processing_status {
- ui.label(format!("🎵 {}/{}", current, total));
+ let status_text = if self.is_paused {
+ format!("⏸ {}/{}", current, total)
+ } else {
+ format!("🎵 {}/{}", current, total)
+ };
+ ui.label(status_text);
ui.add_space(5.0);
}
@@ -521,7 +654,7 @@ impl TtsUi {
} else {
match &self.processing_status {
ProcessingStatus::Completed => {
- ui.label("🎉 All audio chunks have been played successfully!");
+ ui.label("All audio chunks have been played successfully!");
}
ProcessingStatus::Error(err) => {
ui.colored_label(egui::Color32::RED, format!("❌ Error: {}", err));