← Commit history

Publish 1.9.4

John Lauer ·290ad69f3b ·3mo ago ·parent 3c36682
6 files changed +83−18
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.3"+version = "1.9.4" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1
@@ -1 +1 @@-1.9.3+1.9.4
bin/adom-tts
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-tts",-  "version": "1.9.3",+  "version": "1.9.4",   "type": "app",   "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.",   "license": "MIT",
page.json+63−9
@@ -4,24 +4,78 @@   "slug": "adom-tts",   "title": "adom-tts — your AI's voice",   "brief": "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.",-  "version": "1.9.3",+  "version": "1.9.4",   "license": "MIT",-  "tags": ["tts", "voice", "audio", "demo", "accessibility", "open-source", "mit"],+  "tags": [+    "tts",+    "voice",+    "audio",+    "demo",+    "accessibility",+    "cli",+    "open-source",+    "mit"+  ],   "hero": {     "type": "image",     "path": "screenshots/hero-v2.png",     "thumbnail": "screenshots/hero-v2-thumb.png"   },   "discovery_pitch": "Hands-free in the car? Say 'read me your last answer, I'm driving' and adom-tts speaks it aloud. Making a demo video? Feed it your script and get a perfectly-timed narration track in the house voice — no recording booth, no retakes. One shared edge-tts service with Adom pronunciation overrides and an instant source-hash cache.",-  "discovery_triggers": ["adom-tts", "text to speech", "tts", "voiceover", "demo narration", "narration", "synthesize speech", "make audio from text", "read it to me", "read me your answer", "read that back", "read me the last prompt", "I'm driving", "hands-free", "drive mode", "narrate the answer", "play this audio"],+  "discovery_triggers": [+    "adom-tts",+    "text to speech",+    "tts",+    "voiceover",+    "demo narration",+    "narration",+    "synthesize speech",+    "make audio from text",+    "read it to me",+    "read me your answer",+    "read that back",+    "read me the last prompt",+    "I'm driving",+    "hands-free",+    "drive mode",+    "narrate the answer",+    "play this audio"+  ],   "sample_prompts": [-    { "label": "Drive mode", "prompt": "Read me your last answer, I'm driving" },-    { "label": "Demo voiceover", "prompt": "Render this script as a demo voiceover track" },-    { "label": "Synthesize and play", "prompt": "Synthesize a 30-second walkthrough of this design and play it for me" },-    { "label": "Replay last", "prompt": "Play the last TTS clip again" },-    { "label": "Add pronunciation", "prompt": "Teach the TTS service that aci is pronounced A-C-I" }+    {+      "label": "Drive mode",+      "prompt": "Read me your last answer, I'm driving"+    },+    {+      "label": "Demo voiceover",+      "prompt": "Render this script as a demo voiceover track"+    },+    {+      "label": "Synthesize and play",+      "prompt": "Synthesize a 30-second walkthrough of this design and play it for me"+    },+    {+      "label": "Replay last",+      "prompt": "Play the last TTS clip again"+    },+    {+      "label": "Add pronunciation",+      "prompt": "Teach the TTS service that aci is pronounced A-C-I"+    }   ],   "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,+  "org": "adom",+  "author": {+    "name": "John Lauer",+    "email": "[email protected]"   }-}+}\ No newline at end of file
src/play.rs+17−6
@@ -608,8 +608,14 @@ fn wait_for_clip(shared: &Arc<PlayerShared>, id: u64, surface: Surface, url: &st     let mut last_reached = 0u64;     let mut last_advance = Instant::now();     let mut last_reopen = started;+    let mut reopen_count = 0u32;     let mut reopened = false;     loop {+        // If a newer drainer took over the lock, ABANDON this clip NOW — don't+        // keep playing it on our port while the new owner plays on theirs. That+        // overlap (two players at once) is what makes a clip get heard twice.+        // drain()'s own i_own_lock() check then completes our clean shutdown.+        if !i_own_lock() { break; }         // A single drainer means the clip id shouldn't change under us; if it         // somehow did, stop waiting on the stale clip.         if shared.clip.lock().map(|c| c.id).unwrap_or(id) != id { break; }@@ -623,14 +629,19 @@ fn wait_for_clip(shared: &Arc<PlayerShared>, id: u64, surface: Surface, url: &st         // Recover a dead page: if it hasn't polled recently the tab is gone —         // re-open it (throttled to once / 8s) so the clip still gets heard.         let poll_age = shared.poll_age_ms();-        // Tolerate brief mobile-connection jitter: only treat the page as gone-        // after ~9s of silence (a reopen RELOADS the tab and would restart the-        // current clip, so we don't want to fire on a 4-5s hiccup), and re-fire-        // at most every 12s.-        if poll_age > 9000 && now.duration_since(last_reopen) > Duration::from_secs(12) {-            log_event("player_reopen", serde_json::json!({ "id": id, "poll_age_ms": poll_age }));+        // CRITICAL: only ever re-open to rescue a clip that NEVER STARTED+        // (reached == 0 — the tab was dead when the clip began). Re-opening+        // RELOADS the tab, which restarts the clip from 0 — so if we did it+        // after playback began, a flaky mobile connection would make the SAME+        // clip play again and again. Once a clip is progressing we NEVER reopen;+        // a mid-clip connection drop just stalls and we advance to the next clip.+        // Cap rescue attempts so a truly dead tab can't loop-reopen either.+        if reached == 0 && reopen_count < 2+            && poll_age > 9000 && now.duration_since(last_reopen) > Duration::from_secs(12) {+            log_event("player_reopen", serde_json::json!({ "id": id, "poll_age_ms": poll_age, "attempt": reopen_count + 1 }));             open_player_surface(surface, url, play_cfg);             last_reopen = now;+            reopen_count += 1;             reopened = true;         }         if reached > 0 && now.duration_since(last_advance) >= Duration::from_secs(STALL_SECS) {