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.40 — tab flash discipline: alert set with --duration 600 (10-min hard cap survives drainer death) + actively cleared the moment audio finishes, after replays, and at idle close. A flashing tab never outlives its reason.
6 files changed
+32−24
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.39"+version = "1.9.40" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1@@ -1 +1 @@-1.9.39+1.9.40
dev-skills/adom-tts-dev/SKILL.md+5@@ -114,6 +114,11 @@ manually — deploy a new version and let supersede do the handoff. If you must for the full platform facts. - **Settings, not preferences** (Adom-wide word choice; Microsoft/Google/Apple all converged on Settings). `adom-tts settings` / `settings set surface pup`.+- **A flashing tab never outlives its reason** (user rule). The hydrogen tab+ alert is set with `--duration 600` (hydrogen kills the blink at 10 min even+ if the drainer dies) and is actively cleared the moment a clip's audio+ finishes (queue empty), after a History replay, and at idle close. A retry+ keeps the flash: audio is still owed. - **Run `tools/ralph-test.sh` before every release** — see the adom-tts-test skill for the full matrix and the incident behind each test. - **NEVER auto-replay audio the human already heard.** Retry is for TRULY-SILENT
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+11@@ -866,6 +866,8 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> { log_event("clip_start", serde_json::json!({ "clip": name, "port": player.port, "surface": surface.as_str(), "id": id, "replay": true })); let _ = wait_for_clip(&player.shared, id, surface, &url, &cfg.play, now_ms()); let _ = log_clip_end(&player.shared, &name);+ // replay done → the tab's blink has served its purpose+ if surface == Surface::Hydrogen { server::clear_tab_alert(PLAYER_TAB_NAME); } } } continue; // then back to the queue (interrupted clip restarts)@@ -1047,6 +1049,12 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> { let _ = std::fs::rename(&live, &entry); } else { let _ = std::fs::remove_file(&live);+ // Clip reached a terminal outcome → STOP the tab flash NOW+ // (user rule: never keep blinking after the audio is done).+ // A retry keeps it: audio is still owed to the human.+ if surface == Surface::Hydrogen && next_queue_entry(&dir).is_none() {+ server::clear_tab_alert(PLAYER_TAB_NAME);+ } } continue; }@@ -1078,6 +1086,9 @@ pub fn drain(surface_cli: Option<Surface>) -> Result<()> { // Tear the one server down. On supersede, leave the tab alone — the new // owner already reuses it; only close it when WE idled out. log_event("drain_idle_close", serde_json::json!({ "kept_open": keep_open, "superseded": superseded }));+ // Idle close with the tab kept open: nothing is playing and nothing will —+ // make sure it isn't blinking. (On supersede the new owner manages alerts.)+ if !superseded && surface == Surface::Hydrogen { server::clear_tab_alert(PLAYER_TAB_NAME); } player.shutdown(); if !keep_open && !superseded { match surface {
src/server.rs+14−1@@ -482,6 +482,16 @@ fn open_webview_tab(url: &str) -> Result<()> { open_webview_tab_named(url, TAB_NAME) } +/// Stop the tab's blink. Called the MOMENT a clip finishes (the flash's whole+/// reason is "audio is waiting/playing — come listen"; once the audio is done+/// the flash is noise) and when the drainer closes. Belt to the --duration 600+/// suspenders on `alert set` (10-min hard cap even if no one clears).+pub(crate) fn clear_tab_alert(display_name: &str) {+ let _ = Command::new("adom-cli")+ .args(["hydrogen", "alert", "clear", "--name", display_name])+ .output();+}+ pub(crate) fn open_webview_tab_named(url: &str, display_name: &str) -> Result<()> { let workspace = Command::new("adom-cli") .args(["hydrogen", "workspace", "get"])@@ -512,8 +522,11 @@ pub(crate) fn open_webview_tab_named(url: &str, display_name: &str) -> Result<() } // Blink the tab so a foregrounded webview renders + autoplays (a // backgrounded tab is deferred and never plays) and the user notices.+ // --duration 600: hydrogen itself stops the blink after 10 min even if+ // this process dies — a flashing tab must NEVER outlive its reason+ // (user rule). The drainer also clears it the moment audio finishes. let _ = Command::new("adom-cli")- .args(["hydrogen", "alert", "set", "--name", display_name])+ .args(["hydrogen", "alert", "set", "--name", display_name, "--duration", "600"]) .output(); return Ok(()); }