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.54: one surface open per drain (10s debounce), re-offers logged with their reason, T13 asserts a single hbw open, fake page no longer orphans (issue #7)
6 files changed
+41−27
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.53"+version = "1.9.54" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1@@ -1 +1 @@-1.9.53+1.9.54
dev-skills/adom-tts-test/SKILL.md+1−1@@ -41,7 +41,7 @@ and the incident behind each: | T9 port stability | "the port differs on every invocation": a page must reconnect to the same port | | T11 script markers | issue #4 demo narration: `cue` plays exactly one marker as its own clip, marker 2 never auto-advances, a re-cue is accepted (not dedup-refused), `/poll` advertises prefetch, `/seg` serves bytes, `cue-status` is honest | | T12 queue management | issue #4 §2: unscoped `queue clear` refused (exit 5), `--mine` scopes by thread, `queue drop` one, `--all` empties, all dropped clips in History, `control skip` ends the playing clip with verdict `skipped` and no retry |-| T13 hydrogen to desktop fall-through | issue #5: with a fake `adom-cli` (no browser session) and a fake `adom-bridge`, a drainer start must log `surface_fallthrough` to desktop, send `hd_api POST /hbw/open` to the pinned target, record `surface: desktop` in the portfile, and every `clip_end` names the surface that took it |+| T13 hydrogen to desktop fall-through | issue #5: with a fake `adom-cli` (no browser session) and a fake `adom-bridge`, a drainer start must log `surface_fallthrough` to desktop, send `hd_api POST /hbw/open` to the pinned target, record `surface: desktop` in the portfile, every `clip_end` names the surface that took it, and (issue #7) the drain opens the desktop window EXACTLY ONCE: the audience wait's first offer lands as `surface_open_skipped`, never a second `/hbw/open`; later offers log `surface_reoffer` with the reason | | T10 second clip survives the swap | "two threads sent clips and you stopped the playing one": stale telemetry from clip A killed clip B on arrival (falsely "ok"). The `stale-done` fake-page mode replays the blob-download window; B must live >4s and never inherit A's `reached` | Extending: add a `say "TN ..."` block copying an existing test. Trigger the
page.json−21@@ -12,7 +12,6 @@ "audio", "demo", "accessibility",- "cli", "open-source", "mit" ],@@ -65,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+26−1@@ -1233,14 +1233,28 @@ fn queue_depth(dir: &Path) -> usize { /// Open (or refresh) the single player surface. Idempotent — used both to open /// the tab once at startup and to RE-OPEN it if the page later disappears.+/// Wall-clock ms of the last SUCCESSFUL surface open in this drainer. Opens+/// are debounced against it: the startup open and the audience wait's first+/// offer used to land a second apart, which is harmless for a webview tab+/// (open-or-refresh by name) and a pup session (navigate), but opens a SECOND+/// Hydrogen Desktop window every drain (issue #7). One open per 10s, always.+static LAST_SURFACE_OPEN_MS: AtomicU64 = AtomicU64::new(0);+const SURFACE_OPEN_DEBOUNCE_MS: u64 = 10_000;+ fn open_player_surface(surface: Surface, url: &str, play_cfg: &config::PlayConfig) -> bool {+ let since = now_ms().saturating_sub(LAST_SURFACE_OPEN_MS.load(Ordering::SeqCst));+ if since < SURFACE_OPEN_DEBOUNCE_MS {+ log_event("surface_open_skipped", serde_json::json!({ "surface": surface.as_str(), "opened_ms_ago": since,+ "reason": "a surface open succeeded moments ago; one open per 10s (issue #7)" }));+ return true;+ } let opened = match surface { Surface::Hydrogen => server::open_webview_tab_named(url, PLAYER_TAB_NAME), Surface::Pup => open_pup_window(url, &play_cfg.pup_session, &play_cfg.pup_profile), Surface::Desktop => open_desktop_window(url), }; match opened {- Ok(()) => { write_player_surface(surface); true }+ Ok(()) => { write_player_surface(surface); LAST_SURFACE_OPEN_MS.store(now_ms(), Ordering::SeqCst); true } Err(e) => { log_event("open_surface_failed", serde_json::json!({ "surface": surface.as_str(), "error": e.to_string() })); // FALL-THROUGH (issue #5): the hydrogen surface needs a LIVE cloud@@ -1254,6 +1268,7 @@ fn open_player_surface(surface: Surface, url: &str, play_cfg: &config::PlayConfi log_event("surface_fallthrough", serde_json::json!({ "from": "hydrogen", "to": "desktop", "reason": e.to_string() })); eprintln!("# no live Hydrogen editor session: opened the player in Hydrogen Desktop's own window instead (surface desktop)"); write_player_surface(Surface::Desktop);+ LAST_SURFACE_OPEN_MS.store(now_ms(), Ordering::SeqCst); return true; } Err(e2) => {@@ -1381,6 +1396,7 @@ fn wait_for_audience(player: &Player, surface: Surface, url: &str, let started = Instant::now(); let mut last_offer = Instant::now() - Duration::from_secs(60); let mut open_ok_ever = false;+ let mut offers = 0u32; while started.elapsed() < Duration::from_secs(AUDIENCE_WAIT_SECS) { if !i_own_lock() { return Audience::Superseded; } if player.shared.poll_age_ms() < LIVE_MS && sustained_live(&player.shared) {@@ -1389,6 +1405,15 @@ fn wait_for_audience(player: &Player, surface: Surface, url: &str, return Audience::Ready; } if last_offer.elapsed() > Duration::from_secs(15) || started.elapsed() < Duration::from_millis(100) {+ // Every offer after the first is a RETRY and says so in the log,+ // with the reason, so two opens never read as two opens of the+ // same page (issue #7, ask 2). The debounce in open_player_surface+ // turns an offer that follows the startup open into a no-op.+ offers += 1;+ if offers > 1 {+ log_event("surface_reoffer", serde_json::json!({ "surface": surface.as_str(), "attempt": offers,+ "reason": format!("page has not polled for {}s", started.elapsed().as_secs()) }));+ } if open_player_surface(surface, url, play_cfg) { open_ok_ever = true; } last_offer = Instant::now(); }
tools/ralph-test.sh+12−2@@ -45,8 +45,11 @@ sys.stdout.buffer.write(b'ID3' + bytes([4,0,0,0,0,0,0]) + bytes(random.randrange # ---- fake page: polls, downloads, reports progress/pause/done --------------- # fakepage <port> <mode> modes: play (progress→done) | pause-mid | just-poll+# exec: the background job's pid must BE the python page, or `kill $FP` only+# kills the wrapper subshell and an orphan page keeps polling a port the next+# drainer reuses (which hid issue #7 from T13 for two runs). fakepage() {- python3 - "$1" "$2" <<'PY'+ exec python3 - "$1" "$2" <<'PY' import sys,time,json,urllib.request port,mode=sys.argv[1],sys.argv[2] base=f"http://127.0.0.1:{port}"@@ -329,10 +332,17 @@ python3 -c "import json,sys; sys.exit(0 if json.load(open('$T/.adom/tts/player.j # a clip's verdict names the surface that took it mkmp3 $T/t13.mp3 seed-t13 4000 rig play $T/t13.mp3 >/dev/null 2>&1-fakepage $(port) play >/dev/null 2>&1 & FP=$!+# A real Hydrogen Desktop window takes seconds to load and poll; a page that+# answers instantly hides issue #7 (the audience wait never re-offers). Delay it.+(sleep 6; fakepage $(port) play >/dev/null 2>&1) & FP=$! for i in $(seq 1 30); do ev clip_end | grep -q '"t13.mp3"' && break; sleep 1; done kill $FP 2>/dev/null ev clip_end | grep '"t13.mp3"' | grep -q '"surface":"desktop"' && ok "clip_end carries surface=desktop" || bad "clip_end surface: $(ev clip_end | grep t13 | tail -1 | head -c 200)"+# issue #7: the startup open and the audience wait's first offer must not both+# open a Hydrogen Desktop window. Exactly ONE hd_api /hbw/open per drain.+N=$(grep -c '/hbw/open' $FAKE_LOG 2>/dev/null)+[ "$N" -eq 1 ] && ok "desktop window opened exactly once per drain (hd_api /hbw/open x$N)" || bad "hd_api /hbw/open called $N times in one drain (issue #7)"+ev surface_open_skipped | grep -q 'issue #7' && ok "the repeat offer was logged as skipped (not a second open)" || bad "no surface_open_skipped event" # teardown DP=$(python3 -c "print(open('$T/.adom/tts/drainer.lock').read().split()[0])" 2>/dev/null)