← Commit history

Publish 1.4.0

John Lauer ·44884727f6 ·3mo ago ·parent c9d2be9
7 files changed +111−13
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.3.3"+version = "1.4.0" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
SKILL.md+1−1
@@ -189,7 +189,7 @@ detached child handles the surface and self-shuts down on the audio's ## Install (Tier B via wiki)  ```bash-curl -fsSL https://wiki.adom.inc/static/apps/adom-tts/adom-tts \+curl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-tts/adom-tts \   -o /tmp/adom-tts && chmod +x /tmp/adom-tts \   && sudo install -m 0755 /tmp/adom-tts /usr/local/bin/adom-tts \   && adom-tts install \
VERSION+1−1
@@ -1 +1 @@-1.3.3+1.4.0
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-tts",-  "version": "1.3.4",+  "version": "1.4.0",   "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.3.3",+  "version": "1.4.0",   "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+21
@@ -516,6 +516,17 @@ impl ShutdownHandle {     /// the server and join the worker. Returns `true` if `/done` fired     /// (audio actually played to completion), `false` if we timed out.     pub fn wait(mut self) -> bool {+        // Stall/never-start detection so ONE bad clip can't head-of-line-block+        // the whole queue for the full 5-min cap. Uses the page's progress+        // telemetry: if it played then stopped advancing (STALL_SECS), or never+        // started (START_SECS), give up and let the drainer move to the next clip.+        // The player itself also self-heals stalls (reload-on-stuck); this is the+        // backstop for a clip that truly can't play.+        const STALL_SECS: u64 = 14;+        const START_SECS: u64 = 22;+        let started = Instant::now();+        let mut last_reached = 0u64;+        let mut last_advance = Instant::now();         let mut ended_naturally = false;         loop {             if self.done.load(Ordering::SeqCst) {@@ -525,7 +536,17 @@ impl ShutdownHandle {                 ended_naturally = true;                 break;             }+            let reached = self.reached_ms.load(Ordering::SeqCst);+            if reached > last_reached { last_reached = reached; last_advance = Instant::now(); }             let now = Instant::now();+            if reached > 0 && now.duration_since(last_advance) >= Duration::from_secs(STALL_SECS) {+                eprintln!("# clip stalled at {:.1}s (no progress {STALL_SECS}s) — advancing queue", reached as f64 / 1000.0);+                break;+            }+            if reached == 0 && now.duration_since(started) >= Duration::from_secs(START_SECS) {+                eprintln!("# clip never started within {START_SECS}s — skipping so the queue advances");+                break;+            }             if now >= self.max_deadline {                 break;             }
src/player.html+23
@@ -308,6 +308,29 @@   setTimeout(() => { if (a.paused && a.readyState>=2) tryPlay(); updateDiag(); }, 400);   updateDiag(); +  // ---- stall self-heal watchdog ----+  // A browser/proxy hiccup can wedge <audio> in a waiting/readyState<3 loop even+  // with the clip fully buffered — currentTime stops advancing and play() can't+  // un-stick it. Detect "should be playing but isn't advancing" and recover by+  // reloading the source (re-establishes the proxy connection) at the same spot.+  let _wpos = 0, _stalls = 0;+  setInterval(function(){+    if (a.paused || a.ended || !isFinite(a.duration)) { _wpos = a.currentTime; return; }+    if (a.currentTime > _wpos + 0.15) { _wpos = a.currentTime; _stalls = 0; return; } // healthy+    if (a.readyState >= 3) return;                 // buffered enough; just a blip+    _stalls++;+    if (_stalls <= 3) {+      ev('selfheal', '&att=' + _stalls);+      setStatus('recovering…', 'buffering');+      var ct = a.currentTime;+      try { a.load(); a.currentTime = ct; } catch (e) {}+      a.play().catch(function(){});+    } else if (_stalls === 4) {+      setStatus('stalled — press play', 'error');   // give up auto-recovery; let the user retry+      ev('selfheal-giveup', '&t=' + a.currentTime.toFixed(1));+    }+  }, 2500);+   // ---- command channel: poll for CLI-injected commands; no window reload ----   let lastCmdSeq = 0;   setInterval(() => {