app
Adom TTS: Text to Speech
Public Made by Adomby adom
Adom's shared voice service. Reads your AI's answers back to you while you drive, and renders perfectly-timed voiceover tracks for your demo videos, so you never record your own voice.
← Commit history
v1.9.44 — closes #3: every failure/poll path now surfaces the control commands an AI can drive itself. A started-then-stalled clip reports PAUSED explicitly and names 'adom-tts control play'; surface evidence includes the paused state.
5 files changed
+51−32
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-tts"-version = "1.9.43"+version = "1.9.44" dependencies = [ "anyhow", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.43"+version = "1.9.44" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1@@ -1 +1 @@-1.9.43+1.9.44
page.json+1−28@@ -12,7 +12,6 @@ "audio", "demo", "accessibility",- "cli", "open-source", "mit" ],@@ -39,13 +38,7 @@ "hands-free", "drive mode", "narrate the answer",- "play this audio",- "read aloud",- "say it out loud",- "narrate",- "speak the reply",- "talk to me instead of writing",- "drive mode voice"+ "play this audio" ], "sample_prompts": [ {@@ -71,25 +64,5 @@ ], "install": { "binary_name": "adom-tts"- },- "description": "Adom's shared text-to-speech CLI + Playground. Reads your AI answers back to you while driving, and renders perfectly-timed voiceover tracks for demo videos.",- "dependencies": {},- "scripts": {- "install": "./install.sh",- "uninstall": "./uninstall.sh"- },- "needs_sudo": false,- "files": [- "package.json",- "install.sh",- "uninstall.sh",- "bin/adom-tts",- "skills/**",- "completions/**"- ],- "org": "adom",- "author": {- "name": "John Lauer",- "email": "[email protected]" } }
src/play.rs+47−1@@ -351,6 +351,12 @@ pub fn check_clip(clip: &str) -> Result<()> { } else { println!("… QUEUED — \"{clip}\" has {p} clip(s) ahead of it in the shared player."); }+ // A poll loop is exactly where the stuck thread lives; if the player is+ // paused, say so HERE and name the command that resumes it (issue #3).+ if player_is_paused() {+ println!("⏸ the player is PAUSED — nothing will advance until it resumes.");+ println!("# YOU can resume it yourself, no human needed: `adom-tts control play`");+ } println!("# hint: not done yet. Poll `adom-tts check {clip}` again, or next time pass `--confirm`"); println!("# to BLOCK until it plays. Do NOT tell the human it played until this says PLAYED,"); println!("# and do NOT re-send it — queued clips buffer (up to 10 min) + retry (3x) on their own;");@@ -394,6 +400,15 @@ pub fn check_clip(clip: &str) -> Result<()> { std::process::exit(5); } None if started => {+ // THE stuck-thread branch (issue #3): the clip started, then sat.+ // If it is paused, say so and name the command that resumes it —+ // an AI polling here once concluded playback was human-only.+ if player_is_paused() {+ println!("⏸ PAUSED — \"{clip}\" started but the player is paused, so it will not finish on its own.");+ println!("# YOU can resume it yourself, no human needed: `adom-tts control play`");+ println!("# (also: `adom-tts control pause|replay|toggle`, and `adom-tts status` for the queue)");+ return Ok(());+ } println!("▶ PLAYING — \"{clip}\" has started but no completion is logged yet."); println!("# hint: poll `adom-tts check {clip}` again in a few seconds."); Ok(())@@ -449,6 +464,34 @@ fn clip_pending_or_playing(clip: &str) -> bool { /// One-line surface evidence for failure output: what the player page looks /// like RIGHT NOW (live/stale/dead + position), so the caller doesn't need a /// logs+status+check forensics session to know what the listener experienced.+/// Is the live player sitting PAUSED right now? Reported on every failure path+/// so the calling AI is told the ONE command that unblocks it. Field issue #3:+/// a thread's clip sat silent, the hints said "poll and wait, do not re-send",+/// and the AI concluded only a human could resume it — `adom-tts control play`+/// existed the whole time but was never surfaced where the failure was printed.+fn player_is_paused() -> bool {+ let Some(port) = read_player_port() else { return false };+ reqwest::blocking::Client::new()+ .get(format!("http://127.0.0.1:{port}/state"))+ .timeout(Duration::from_millis(500)).send().ok()+ .and_then(|r| r.json::<serde_json::Value>().ok())+ .and_then(|v| v.get("paused").and_then(|p| p.as_bool()))+ .unwrap_or(false)+}++/// The control commands an AI can drive itself. Printed on failure paths so a+/// stuck thread never again concludes that playback is human-only.+fn control_hint_lines() -> Vec<String> {+ let mut out = Vec::new();+ if player_is_paused() {+ out.push("# The player is PAUSED (a human tapped pause, or a clip was held).".to_string());+ out.push("# YOU can resume it yourself, no human needed: `adom-tts control play`".to_string());+ } else {+ out.push("# You can drive the player yourself: `adom-tts control play|pause|replay|toggle`".to_string());+ }+ out+}+ fn surface_evidence() -> String { let Some(port) = read_player_port() else { return "no live player server".into() }; match reqwest::blocking::Client::new()@@ -459,7 +502,8 @@ fn surface_evidence() -> String { let age = v.get("poll_age_ms").and_then(|x| x.as_u64()).unwrap_or(999_999_999); let pipe = if age < 3000 { format!("page LIVE (polled {age}ms ago)") } else { format!("page NOT polling ({}s silent — listener's device offline/backgrounded)", age / 1000) };- format!("player :{port}, {pipe}, current clip \"{}\" at {:.0}s",+ let paused = if v.get("paused").and_then(|p| p.as_bool()).unwrap_or(false) { ", PAUSED" } else { "" };+ format!("player :{port}, {pipe}, current clip \"{}\" at {:.0}s{paused}", v.get("clip").and_then(|c| c.as_str()).unwrap_or("-"), v.get("last_ms").and_then(|x| x.as_f64()).unwrap_or(0.0) / 1000.0) }@@ -503,6 +547,7 @@ pub fn confirm_playback(clip: &str, since_ms: u64, timeout_secs: u64) -> Result< } eprintln!("# hint: the human did NOT hear this. Do NOT tell them it played. Fix the surface first"); eprintln!("# (`adom-tts reopen` if the page is dead), then re-send ONCE with `--confirm`.");+ for l in control_hint_lines() { eprintln!("{l}"); } std::process::exit(5); } break; // retry pending — keep waiting for its outcome@@ -520,6 +565,7 @@ pub fn confirm_playback(clip: &str, since_ms: u64, timeout_secs: u64) -> Result< eprintln!(" surface: {}", surface_evidence()); eprintln!("# hint: clips buffer up to 10 min + retry 3x on their own. Poll `adom-tts check {clip}`"); eprintln!("# until PLAYED/FAILED; do NOT re-send yet — a duplicate would play twice on reconnect.");+ for l in control_hint_lines() { eprintln!("{l}"); } std::process::exit(5); } thread::sleep(Duration::from_millis(700));