← 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.

John Lauer ·34f8474b32 ·1mo ago ·parent 0c70474
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]"   } }\ No newline at end of file
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));