← All Pull Requests

Content fix: retire dead wiki-*.adom.cloud host + gallia references #12

Open opened by Drew Owens 2026-07-15

Housecleaning sweep (per Drew): the old wiki-ufypy5dpx93o.adom.cloud host and gallia/install.mjs are decommissioned — wiki.adom.inc + the adom-wiki CLI are canonical. This PR updates the install one-liner to 'adom-wiki pkg install adom/adom-tts' and re-points the pronunciation-table contribution flow at the wiki page. Docs-only; no code changes. Note: build.sh and src/ still reference the dead host — left for the owner to handle with a rebuild. Owner's call to merge.

Diff

--- a/skills/adom-tts/SKILL.md+++ b/skills/adom-tts/SKILL.md@@ -1,341 +1,340 @@-----name: adom-tts-description: >-  Thin Rust CLI for service-tts — Adom's shared edge-tts wrapper with-  pronunciation overrides + source-hash cache. Every Adom tool that-  renders narration (demo-recording, any walkthrough/tour)-  should shell to `adom-tts say` instead of calling edge-tts directly,-  so Adom terms like adom-tsci come out phonetically right and repeat-  synthesis hits the cache.-  Trigger words: adom-tts, text to speech, tts, narration, voiceover,-  synthesize speech, edge-tts, neural voice, andrew neural,-  pronunciation override, demo narration, walkthrough narration,-  say this, speak this, make audio from text, render narration,-  voice service, service-tts,-  play this audio, play the audio file, auto-play tts, narrate hands-free,-  drive mode, drive-mode tts, read this to me, play it back, replay last tts.-  HANDS-FREE TRIGGERS — when the user says ANY of these, ALWAYS reach for-  `adom-tts say --play` instead of writing a long text response:-  "I'm driving", "I am driving", "while I drive", "while driving",-  "can't read the screen", "can't see the screen", "can't look at the screen",-  "read it to me", "read this to me", "read it out loud", "read me your answer",-  "read me back", "read me the", "read me that", "read me that last prompt",-  "read me the last prompt", "read me what you just said", "read your last answer",-  "read your last response", "read that back", "read that back to me",-  "read me your last", "read back the last prompt", "narrate the answer",-  "say it out loud", "tell me out loud", "speak the answer", "audio answer please",-  "I'm in the car", "in the car right now", "on my commute", "hands-free".-  These phrases mean the user CANNOT see your text response — synthesize it-  and auto-play. Default surface is hydrogen webview; if the user seems to be-  on a phone via /remote-control, hydrogen still works (audio plays through phone).------# adom-tts — shared TTS CLI, Tier B--`adom-tts` is the Adom-wide text-to-speech CLI. It shells every synthesis-call through the `service-tts` container (edge-tts + `en-US-AndrewNeural`-by default) so:--1. **Adom pronunciation overrides auto-apply server-side.** Terms like-   `adom-tsci`, `instapcb`, `kicad` come out phonetically correct-   without the caller pre-phoneticizing. 18 overrides loaded from-   [tts-pronunciation](../tts-pronunciation/SKILL.md).-2. **Source-hash cache.** Repeat synthesis of the same `(text, voice, rate)`-   returns cached bytes instantly. Demo iteration loops save real time.-3. **Adom-owned URL.** Backend (edge-tts → piper → Azure direct → ElevenLabs)-   is swappable without any caller changes.--Default voice is `en-US-AndrewNeural` per approval. Service lives at-`https://adom-tts-f84dy9x2ezq9.adom.cloud`; CLI bakes it in at build time.--## When to reach for this skill (besides obvious "make me an mp3" asks)--**Hands-free / driving mode.** If the user says any of "I'm driving", "can't read the screen", "read it to me", "read it out loud", "read me your answer", "I'm in the car", "narrate the answer", "tell me out loud", "while I drive" — they CANNOT see your text response. The right move is:--1. Compose what you would have written as a normal reply (concise, ~30–60 seconds spoken).-2. Speak it with the CANONICAL verified pattern — identify your thread, block until heard:-   ```bash-   adom-tts say "This is the <thread> thread — <answer>" \-     --out /tmp/answer.mp3 --thread "<thread>" \-     --desc "<ONE short sentence: what this update says>" --play --confirm-   ```-3. `--confirm` exits 0 ONLY when the human actually heard it (`✓ CONFIRMED`).-   On `✗ NOT CONFIRMED`/`UNCONFIRMED`: do NOT re-send — clips buffer + retry on-   their own, and a duplicate would play twice on reconnect. Poll-   `adom-tts check answer.mp3` until it says PLAYED (or FAILED → then report).-4. No separate "here's a summary" text response on top (the user can't see it anyway).--This is the canonical drive-mode pattern. Memorize it. The user's literal words won't always say "TTS" — they'll say "I'm driving, just read me your answer." That IS the trigger.--## Use--**If the user wants to HEAR the audio, use `--play`. Don't author a custom autoplay HTML, don't `python3 -m http.server`, don't create a port mapping. `adom-tts` ships a built-in auto-player.**--```bash-# The common case — synthesize, AUTO-PLAY, and VERIFY the human heard it.-# Blocks until ✓ CONFIRMED (exit 0) or fails loudly (exit ≠0). Use this form-# whenever a human is supposed to HEAR the clip (always, when hands-free).-adom-tts say "This is the <thread> thread — the answer." --out /tmp/x.mp3 --thread "<thread>" --desc "what this clip says, in one line" --play --confirm--# Fire-and-forget variant (pipelines where blocking is bad): returns instantly-# with "ACCEPTED — position N". You MUST then poll `adom-tts check x.mp3` until-# PLAYED before telling the human you spoke it.-adom-tts say "Read me the answer." --out /tmp/x.mp3 --play--# --surface pup if the user prefers a desktop browser window instead.-adom-tts say "..." --out /tmp/x.mp3 --play --surface pup--# Replay the newest clip from ~/.adom/tts/history/ (e.g. user said "play that again").-adom-tts play last--# Synthesize without playing (rare — only when you genuinely just need the file).-adom-tts say "Hello from adom-tsci" --out hello.mp3--# Pipe from stdin for long narration.-cat narration.txt | adom-tts say - --out narration.mp3 --play--# Different voice / speed.-adom-tts say "Announcement" --out a.mp3 --voice en-US-AriaNeural --play-adom-tts say "Quickly" --out q.mp3 --rate +15% --play--# Raw text (no pronunciation overrides — rare, for non-Adom narration).-adom-tts say "Generic sentence" --out g.mp3 --no-pronunciations --play-```--**Anti-patterns (have caused real spin-loops in past sessions):**-- Do NOT: `python3 -m http.server <port>` to serve the mp3 — pup runs on the user's *desktop*, container localhost is unreachable. Hydrogen webviews have the same constraint.-- Do NOT: custom HTML with base64-inlined mp3 + `data:` URL — past 2 MB this hits Chrome's URL length limit.-- Do NOT: `adom-cli carbon containers port-add` — provisions a permanent `*.adom.cloud` subdomain for a one-shot, total overkill.-- DO: just use `--play`. The binary handles the surface, the URL, and cleanup.--**NEVER install a local TTS engine as a fallback.** If `adom-tts` exits-non-zero — `adom-tts: unreachable` (exit 2) or `HTTP <status>` (exit 4) — or-`adom-tts health` fails, the service-tts container is down. The correct-response is to **STOP and report the outage to the user**, then retry later.-It is a shared service; an Adom administrator fixes it once for everyone.--Do **NOT** "work around" the outage by installing `edge-tts`, `piper`,-`espeak`/`espeak-ng`, `festival`, `pico2wave`, `coqui-tts`, or any local-synthesis engine (`pip install edge-tts`, `apt-get install espeak`, `npm i-say`, etc.). The binary itself has **no** local-synthesis fallback by design —-neither should you. Installing one:-- pollutes the cloud container with a long-lived dependency the user never-  asked for (and the user has explicitly said they do NOT want this — for-  themselves OR for other users),-- skips the server-side Adom pronunciation overrides (so `adom-tsci`,-  `instapcb`, `kicad` come out wrong),-- drifts off the approved house voice (`en-US-AndrewNeural`),-- and on this PID-1 / code-server container, running installers risks-  breaking the container outright.--Unreachable service = report it, full stop. No local install, ever.--`say` emits `→ <path> (<bytes>, cache=hit|miss, hash=<prefix>)` — scripts-can parse `cache=hit` to detect re-renders.--When `--play` is omitted, `say` also emits stderr hints reminding the AI of the auto-player. Silence them with `ADOM_TTS_NO_HINTS=1` for already-stable scripts.--Other commands:--```bash-adom-tts voices          # GET /tts/voices (edge-tts list as JSON)-adom-tts pronunciations  # GET /tts/pronunciations (the override table)-adom-tts health          # service + edge-tts version-adom-tts config          # which URL is this binary hitting?-adom-tts config --show   # also dump the resolved play surface + config path-```--## Capabilities & commands (v1.9+)--**`--play` REQUIRES `--thread` AND `--desc` (exit 6 otherwise).** Many AI-threads share one player and one replayable History; the human can't tell-`reply.mp3` clips apart. `--thread` = your thread's short name; `--desc` = ONE-plain sentence describing the update ("login fix deployed", "flight prices-found"). Both show in the player's **History drawer**, where the human can see-what they missed (missed clips are flagged) and tap ↻ to replay any clip —-so write the desc for a human scanning a list, not for a log.--**One persistent player = a broker across ALL threads.** Every `say --play` /-`play` from every Claude thread feeds ONE shared player and clips play-**sequentially — never overlapping**. A SINGLE long-lived server (one drain-lock, one stable port) owns the one webview for its whole life and swaps each-clip's audio **without reloading the tab** — so clips never "flip" between each-other and a new clip can't leave the tab silent. If the binary is upgraded, a-newer drainer **supersedes** the old one (older-version drainer self-exits), so-every thread converges on the latest build. The player header shows the running-**version badge** (spot any thread still on an old build) and reflows fluidly to-any width (skinny 20% column → full screen). You just call `say --play`. A stalled/failed clip no longer blocks the queue (it-self-advances in ~15s) and the player **self-heals** a stalled stream by-reloading it. Your call returns immediately with an **`ACCEPTED "<clip>" —-position N`** acknowledgement — it does NOT block other threads.--**VERIFY that the human actually heard it — this is mandatory.** `say --play`-only *queues* the clip; it does not prove playback. Never tell the human you-spoke something until you've verified it. Three ways, strongest first:-```bash-adom-tts say "..." --out /tmp/x.mp3 --thread "<t>" --play --confirm  # BLOCKS until played; exit≠0 if not-adom-tts check x.mp3     # non-blocking: prints QUEUED / PLAYING / PLAYED / FAILED; exit≠0 unless PLAYED-adom-tts status          # broker view: is a player live, what's playing now, whole queue in order-```-For hands-free / driving, **`--confirm` is THE canonical path** — it is reliable-under a flaky phone connection because it simply watches the playback log (it-never touches the contended surface itself) and reports the truth or times out-loudly. **Never-lose-a-clip (v1.9.8+):** a clip the page never fetched-(no-playback) is NOT consumed — it retries up to 3 times, each behind a-sustained-liveness gate (3 consecutive polls) so it can't restart into a dead page (hotspot drop, locked phone, backgrounded tab). Plain `say --play` also now warns `⚠ AUDIENCE OFFLINE` at send time when-no page is listening, and `adom-tts reopen` re-offers a dead player tab in one-command. **Env self-heal (v1.9.11+):** a `say` from a sanitized shell (cron/hooks) used-to spawn a player that couldn't reach Hydrogen (`workspace get failed` → every-clip no-playback). Now every healthy caller persists its surface env-(`~/.adom/tts/env.json`) and the player adopts missing vars at startup — one-bad shell can no longer poison playback. `adom-tts reopen` = one-shot recover.-**Hotspot-proof playback (v1.9.9+):** the page downloads each clip fully and-plays from a local blob, so a cellular-hotspot drop mid-clip can't stall audio;-telemetry rides on the poll, so the first poll after a gap restores the true-verdict (no false "partial" for a clip the human fully heard).-**Drive-mode buffering (v1.9.5+):** if the listener's-device (phone ↔ Hydrogen) is disconnected, clips are NOT burned — they buffer in-order up to 10 min and play when it reconnects. So an UNCONFIRMED timeout means-"not heard YET": poll `adom-tts check <clip>` later and do NOT re-send (a-duplicate would play twice on reconnect). In a pipeline where blocking is bad, fire with `--play`-then poll `adom-tts check <clip>` until it says `✓ PLAYED`. On `FAILED`, re-queue-and report the outage — **never** install a local TTS fallback (shared-infra rule).--**Identify your thread.** Clips from parallel threads share the player, so the-listener needs to know who's speaking:-```bash-adom-tts say "This is the <thread> thread — <answer>" --out /tmp/x.mp3 --thread "<thread>" --play-```-`--thread "<name>"` labels the clip in the player header. ALSO begin your spoken-text by naming the thread ("This is the brand thread — …") so it's heard aloud.--**Long text is auto-chunked.** service-tts drops long single requests, so `say`-auto-splits text >~700 chars at sentence boundaries, synthesizes each short piece-(fast, cached, retried on flaps), and stitches them into ONE seamless clip. Pass-the full paragraph — do NOT pre-chunk. Transient service failures are retried-automatically; **never install a local TTS engine** (shared-infra rule).--**Control the live player without reloading it** (great for hands-free "play-that again"):-```bash-adom-tts control replay    # jump to start + play-adom-tts control pause-adom-tts control play-adom-tts control toggle-```--**Review what happened.** Every synth + playback + queue event is logged:-```bash-adom-tts logs [--tail N]    # events + summary + anomaly hints-```-Use it to diagnose silent clips, service flaps, or multiple threads/containers-competing for the shared player. The player header also shows a **live-link LED**-(green = live pipe to the AI, red = dead/orphaned tab).--## Drive mode (hands-free auto-player)--You wrote an mp3 — now play it. Don't author a custom autoplay HTML.--```bash-# One shot: synthesize and auto-play. Detaches; CLI returns immediately.-adom-tts say "Read me back the answer." --out /tmp/x.mp3 --play--# Replay the most recent clip from ~/.adom/tts/history/.-adom-tts play last--# Replay a specific clip by full id or any unique substring of its hash.-adom-tts play 20260506-104555-c55f79f0-adom-tts play 5f79f0--# Block until playback finishes (--wait, max 5 min) — useful for shell scripts-# that need to chain after the audio ends.-adom-tts play /tmp/x.mp3 --wait-```--**How it works.** `play` (and `say --play`) spins up a one-shot HTTP-server bound to a 127.0.0.1 ephemeral port, serving the mp3 + a tiny-autoplay page. The container's `$VSCODE_PROXY_URI` wraps that port into-a Cloudflare-routable URL. The chosen surface (Hydrogen webview tab or-pup browser window) navigates to that URL — same plumbing, no port-mappings, no clip-size limits, no base64 URL bloat.--**Surface (Hydrogen vs pup).** Picked from this precedence chain:--```---surface hydrogen|pup   →   $ADOM_TTS_SURFACE   →   ~/.adom/tts/config.toml   →   "hydrogen" (default)-```--Pin a default in `~/.adom/tts/config.toml`:--```toml-[play]-surface     = "pup"        # or "hydrogen"-pup_session = "adom-tts"-pup_profile = "adom-tts"-```--Default is `hydrogen` (always available inside the container; pup needs-a desktop Chrome session). Per the pup skill's "pick ONE surface" rule,-there is intentionally no `both` option.--**Tab name.** Hydrogen tabs are titled `"TTS Player"` (distinct from the-playground's `"TTS Playground"`). Repeat `play` calls open new tabs;-close stale ones with `adom-cli hydrogen workspace close-panel <id>`.--**For AIs.** When the user says "read me X", "play this", "make audio-and play it" — go straight to `adom-tts say "X" --out /tmp/x.mp3 --play`.-Don't write an autoplay HTML file. Don't create a port mapping. The-detached child handles the surface and self-shuts down on the audio's-`ended` event (or after a 5-minute cap).--## Install (Tier B via wiki)--```bash-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 \-  && adom-tts health-```--## Who should use this (and how)--- **demo-recording** — replace `edge-tts --voice ...` invocations with-  `adom-tts say`. Cache hit + phonetic-correct Adom terms for free.-- **any future tour/walkthrough/voiceover** — same pattern: `adom-tts say`,-  don't invoke edge-tts directly.--## Why not invoke edge-tts directly?--Because:--- Every consumer has to know every pronunciation override (or get them-  wrong). Server-side application fixes this once.-- Repeat renders cost real time (+ Microsoft endpoint bandwidth). A-  source-hash cache makes the 2nd + N-th call free.-- When Microsoft changes the edge-tts endpoint (they have; will again),-  every consumer breaks in parallel. One server-side swap fixes everyone.-- Voice consistency: Andrew Neural is approved once in the `Adom` context;-  every call through adom-tts gets that default.--## Related skills--- [tts-pronunciation](../tts-pronunciation/SKILL.md) — the canonical-  phonetic-override table (loaded by service-tts at startup).-- [demo-recording](../demo-recording/SKILL.md) — top consumer.-- [service-kicad](../service-kicad/SKILL.md),-  [step2glb](../step2glb/SKILL.md) — sibling Tier B services + CLIs.--Canonical source: `adom-inc/service-tts` repo + `adom-inc/adom-tts` repo.--## Canonical voice: en-US-AndrewNeural--The standard voice for every Adom demo, walkthrough, or narration is `en-US-AndrewNeural`. Use it as the default in `adom-tts say` unless the user explicitly asks for variety. It's been confirmed on the aci feature-tour demo and several walkthrough recordings as the right balance of clarity, neutrality, and warmth.--## Skip ElevenLabs premades — pull from Shared Voice Library--The default 21-voice ElevenLabs set sounds like AI-generated YouTube narration. When considering an ElevenLabs voice for a specific project, pull from the **Shared Voice Library** sorted by `cloned_by_count`, biased toward husky/raspy/character/accent voices. Andrew Neural remains the bar to beat.--## Audio playback via pup, not new port mappings--For one-off audio playback (TTS test, narration preview), base64-inline the mp3 into a small auto-play HTML, then `browser_open_window` with a data URL, then force-play with `browser_eval` if Chrome's autoplay policy blocks it. Don't create a new public port mapping just to serve one audio file. Past pattern: `port-add 8888` for a one-time TTS preview, never cleaned up.+---+name: adom-tts+description: >+  Thin Rust CLI for service-tts — Adom's shared edge-tts wrapper with+  pronunciation overrides + source-hash cache. Every Adom tool that+  renders narration (demo-recording, any walkthrough/tour)+  should shell to `adom-tts say` instead of calling edge-tts directly,+  so Adom terms like adom-tsci come out phonetically right and repeat+  synthesis hits the cache.+  Trigger words: adom-tts, text to speech, tts, narration, voiceover,+  synthesize speech, edge-tts, neural voice, andrew neural,+  pronunciation override, demo narration, walkthrough narration,+  say this, speak this, make audio from text, render narration,+  voice service, service-tts,+  play this audio, play the audio file, auto-play tts, narrate hands-free,+  drive mode, drive-mode tts, read this to me, play it back, replay last tts.+  HANDS-FREE TRIGGERS — when the user says ANY of these, ALWAYS reach for+  `adom-tts say --play` instead of writing a long text response:+  "I'm driving", "I am driving", "while I drive", "while driving",+  "can't read the screen", "can't see the screen", "can't look at the screen",+  "read it to me", "read this to me", "read it out loud", "read me your answer",+  "read me back", "read me the", "read me that", "read me that last prompt",+  "read me the last prompt", "read me what you just said", "read your last answer",+  "read your last response", "read that back", "read that back to me",+  "read me your last", "read back the last prompt", "narrate the answer",+  "say it out loud", "tell me out loud", "speak the answer", "audio answer please",+  "I'm in the car", "in the car right now", "on my commute", "hands-free".+  These phrases mean the user CANNOT see your text response — synthesize it+  and auto-play. Default surface is hydrogen webview; if the user seems to be+  on a phone via /remote-control, hydrogen still works (audio plays through phone).+---++# adom-tts — shared TTS CLI, Tier B++`adom-tts` is the Adom-wide text-to-speech CLI. It shells every synthesis+call through the `service-tts` container (edge-tts + `en-US-AndrewNeural`+by default) so:++1. **Adom pronunciation overrides auto-apply server-side.** Terms like+   `adom-tsci`, `instapcb`, `kicad` come out phonetically correct+   without the caller pre-phoneticizing. 18 overrides loaded from+   [tts-pronunciation](../tts-pronunciation/SKILL.md).+2. **Source-hash cache.** Repeat synthesis of the same `(text, voice, rate)`+   returns cached bytes instantly. Demo iteration loops save real time.+3. **Adom-owned URL.** Backend (edge-tts → piper → Azure direct → ElevenLabs)+   is swappable without any caller changes.++Default voice is `en-US-AndrewNeural` per approval. Service lives at+`https://adom-tts-f84dy9x2ezq9.adom.cloud`; CLI bakes it in at build time.++## When to reach for this skill (besides obvious "make me an mp3" asks)++**Hands-free / driving mode.** If the user says any of "I'm driving", "can't read the screen", "read it to me", "read it out loud", "read me your answer", "I'm in the car", "narrate the answer", "tell me out loud", "while I drive" — they CANNOT see your text response. The right move is:++1. Compose what you would have written as a normal reply (concise, ~30–60 seconds spoken).+2. Speak it with the CANONICAL verified pattern — identify your thread, block until heard:+   ```bash+   adom-tts say "This is the <thread> thread — <answer>" \+     --out /tmp/answer.mp3 --thread "<thread>" \+     --desc "<ONE short sentence: what this update says>" --play --confirm+   ```+3. `--confirm` exits 0 ONLY when the human actually heard it (`✓ CONFIRMED`).+   On `✗ NOT CONFIRMED`/`UNCONFIRMED`: do NOT re-send — clips buffer + retry on+   their own, and a duplicate would play twice on reconnect. Poll+   `adom-tts check answer.mp3` until it says PLAYED (or FAILED → then report).+4. No separate "here's a summary" text response on top (the user can't see it anyway).++This is the canonical drive-mode pattern. Memorize it. The user's literal words won't always say "TTS" — they'll say "I'm driving, just read me your answer." That IS the trigger.++## Use++**If the user wants to HEAR the audio, use `--play`. Don't author a custom autoplay HTML, don't `python3 -m http.server`, don't create a port mapping. `adom-tts` ships a built-in auto-player.**++```bash+# The common case — synthesize, AUTO-PLAY, and VERIFY the human heard it.+# Blocks until ✓ CONFIRMED (exit 0) or fails loudly (exit ≠0). Use this form+# whenever a human is supposed to HEAR the clip (always, when hands-free).+adom-tts say "This is the <thread> thread — the answer." --out /tmp/x.mp3 --thread "<thread>" --desc "what this clip says, in one line" --play --confirm++# Fire-and-forget variant (pipelines where blocking is bad): returns instantly+# with "ACCEPTED — position N". You MUST then poll `adom-tts check x.mp3` until+# PLAYED before telling the human you spoke it.+adom-tts say "Read me the answer." --out /tmp/x.mp3 --play++# --surface pup if the user prefers a desktop browser window instead.+adom-tts say "..." --out /tmp/x.mp3 --play --surface pup++# Replay the newest clip from ~/.adom/tts/history/ (e.g. user said "play that again").+adom-tts play last++# Synthesize without playing (rare — only when you genuinely just need the file).+adom-tts say "Hello from adom-tsci" --out hello.mp3++# Pipe from stdin for long narration.+cat narration.txt | adom-tts say - --out narration.mp3 --play++# Different voice / speed.+adom-tts say "Announcement" --out a.mp3 --voice en-US-AriaNeural --play+adom-tts say "Quickly" --out q.mp3 --rate +15% --play++# Raw text (no pronunciation overrides — rare, for non-Adom narration).+adom-tts say "Generic sentence" --out g.mp3 --no-pronunciations --play+```++**Anti-patterns (have caused real spin-loops in past sessions):**+- Do NOT: `python3 -m http.server <port>` to serve the mp3 — pup runs on the user's *desktop*, container localhost is unreachable. Hydrogen webviews have the same constraint.+- Do NOT: custom HTML with base64-inlined mp3 + `data:` URL — past 2 MB this hits Chrome's URL length limit.+- Do NOT: `adom-cli carbon containers port-add` — provisions a permanent `*.adom.cloud` subdomain for a one-shot, total overkill.+- DO: just use `--play`. The binary handles the surface, the URL, and cleanup.++**NEVER install a local TTS engine as a fallback.** If `adom-tts` exits+non-zero — `adom-tts: unreachable` (exit 2) or `HTTP <status>` (exit 4) — or+`adom-tts health` fails, the service-tts container is down. The correct+response is to **STOP and report the outage to the user**, then retry later.+It is a shared service; an Adom administrator fixes it once for everyone.++Do **NOT** "work around" the outage by installing `edge-tts`, `piper`,+`espeak`/`espeak-ng`, `festival`, `pico2wave`, `coqui-tts`, or any local+synthesis engine (`pip install edge-tts`, `apt-get install espeak`, `npm i+say`, etc.). The binary itself has **no** local-synthesis fallback by design —+neither should you. Installing one:+- pollutes the cloud container with a long-lived dependency the user never+  asked for (and the user has explicitly said they do NOT want this — for+  themselves OR for other users),+- skips the server-side Adom pronunciation overrides (so `adom-tsci`,+  `instapcb`, `kicad` come out wrong),+- drifts off the approved house voice (`en-US-AndrewNeural`),+- and on this PID-1 / code-server container, running installers risks+  breaking the container outright.++Unreachable service = report it, full stop. No local install, ever.++`say` emits `→ <path> (<bytes>, cache=hit|miss, hash=<prefix>)` — scripts+can parse `cache=hit` to detect re-renders.++When `--play` is omitted, `say` also emits stderr hints reminding the AI of the auto-player. Silence them with `ADOM_TTS_NO_HINTS=1` for already-stable scripts.++Other commands:++```bash+adom-tts voices          # GET /tts/voices (edge-tts list as JSON)+adom-tts pronunciations  # GET /tts/pronunciations (the override table)+adom-tts health          # service + edge-tts version+adom-tts config          # which URL is this binary hitting?+adom-tts config --show   # also dump the resolved play surface + config path+```++## Capabilities & commands (v1.9+)++**`--play` REQUIRES `--thread` AND `--desc` (exit 6 otherwise).** Many AI+threads share one player and one replayable History; the human can't tell+`reply.mp3` clips apart. `--thread` = your thread's short name; `--desc` = ONE+plain sentence describing the update ("login fix deployed", "flight prices+found"). Both show in the player's **History drawer**, where the human can see+what they missed (missed clips are flagged) and tap ↻ to replay any clip —+so write the desc for a human scanning a list, not for a log.++**One persistent player = a broker across ALL threads.** Every `say --play` /+`play` from every Claude thread feeds ONE shared player and clips play+**sequentially — never overlapping**. A SINGLE long-lived server (one drain+lock, one stable port) owns the one webview for its whole life and swaps each+clip's audio **without reloading the tab** — so clips never "flip" between each+other and a new clip can't leave the tab silent. If the binary is upgraded, a+newer drainer **supersedes** the old one (older-version drainer self-exits), so+every thread converges on the latest build. The player header shows the running+**version badge** (spot any thread still on an old build) and reflows fluidly to+any width (skinny 20% column → full screen). You just call `say --play`. A stalled/failed clip no longer blocks the queue (it+self-advances in ~15s) and the player **self-heals** a stalled stream by+reloading it. Your call returns immediately with an **`ACCEPTED "<clip>" —+position N`** acknowledgement — it does NOT block other threads.++**VERIFY that the human actually heard it — this is mandatory.** `say --play`+only *queues* the clip; it does not prove playback. Never tell the human you+spoke something until you've verified it. Three ways, strongest first:+```bash+adom-tts say "..." --out /tmp/x.mp3 --thread "<t>" --play --confirm  # BLOCKS until played; exit≠0 if not+adom-tts check x.mp3     # non-blocking: prints QUEUED / PLAYING / PLAYED / FAILED; exit≠0 unless PLAYED+adom-tts status          # broker view: is a player live, what's playing now, whole queue in order+```+For hands-free / driving, **`--confirm` is THE canonical path** — it is reliable+under a flaky phone connection because it simply watches the playback log (it+never touches the contended surface itself) and reports the truth or times out+loudly. **Never-lose-a-clip (v1.9.8+):** a clip the page never fetched+(no-playback) is NOT consumed — it retries up to 3 times, each behind a+sustained-liveness gate (3 consecutive polls) so it can't restart into a dead page (hotspot drop, locked phone, backgrounded tab). Plain `say --play` also now warns `⚠ AUDIENCE OFFLINE` at send time when+no page is listening, and `adom-tts reopen` re-offers a dead player tab in one+command. **Env self-heal (v1.9.11+):** a `say` from a sanitized shell (cron/hooks) used+to spawn a player that couldn't reach Hydrogen (`workspace get failed` → every+clip no-playback). Now every healthy caller persists its surface env+(`~/.adom/tts/env.json`) and the player adopts missing vars at startup — one+bad shell can no longer poison playback. `adom-tts reopen` = one-shot recover.+**Hotspot-proof playback (v1.9.9+):** the page downloads each clip fully and+plays from a local blob, so a cellular-hotspot drop mid-clip can't stall audio;+telemetry rides on the poll, so the first poll after a gap restores the true+verdict (no false "partial" for a clip the human fully heard).+**Drive-mode buffering (v1.9.5+):** if the listener's+device (phone ↔ Hydrogen) is disconnected, clips are NOT burned — they buffer in+order up to 10 min and play when it reconnects. So an UNCONFIRMED timeout means+"not heard YET": poll `adom-tts check <clip>` later and do NOT re-send (a+duplicate would play twice on reconnect). In a pipeline where blocking is bad, fire with `--play`+then poll `adom-tts check <clip>` until it says `✓ PLAYED`. On `FAILED`, re-queue+and report the outage — **never** install a local TTS fallback (shared-infra rule).++**Identify your thread.** Clips from parallel threads share the player, so the+listener needs to know who's speaking:+```bash+adom-tts say "This is the <thread> thread — <answer>" --out /tmp/x.mp3 --thread "<thread>" --play+```+`--thread "<name>"` labels the clip in the player header. ALSO begin your spoken+text by naming the thread ("This is the brand thread — …") so it's heard aloud.++**Long text is auto-chunked.** service-tts drops long single requests, so `say`+auto-splits text >~700 chars at sentence boundaries, synthesizes each short piece+(fast, cached, retried on flaps), and stitches them into ONE seamless clip. Pass+the full paragraph — do NOT pre-chunk. Transient service failures are retried+automatically; **never install a local TTS engine** (shared-infra rule).++**Control the live player without reloading it** (great for hands-free "play+that again"):+```bash+adom-tts control replay    # jump to start + play+adom-tts control pause+adom-tts control play+adom-tts control toggle+```++**Review what happened.** Every synth + playback + queue event is logged:+```bash+adom-tts logs [--tail N]    # events + summary + anomaly hints+```+Use it to diagnose silent clips, service flaps, or multiple threads/containers+competing for the shared player. The player header also shows a **live-link LED**+(green = live pipe to the AI, red = dead/orphaned tab).++## Drive mode (hands-free auto-player)++You wrote an mp3 — now play it. Don't author a custom autoplay HTML.++```bash+# One shot: synthesize and auto-play. Detaches; CLI returns immediately.+adom-tts say "Read me back the answer." --out /tmp/x.mp3 --play++# Replay the most recent clip from ~/.adom/tts/history/.+adom-tts play last++# Replay a specific clip by full id or any unique substring of its hash.+adom-tts play 20260506-104555-c55f79f0+adom-tts play 5f79f0++# Block until playback finishes (--wait, max 5 min) — useful for shell scripts+# that need to chain after the audio ends.+adom-tts play /tmp/x.mp3 --wait+```++**How it works.** `play` (and `say --play`) spins up a one-shot HTTP+server bound to a 127.0.0.1 ephemeral port, serving the mp3 + a tiny+autoplay page. The container's `$VSCODE_PROXY_URI` wraps that port into+a Cloudflare-routable URL. The chosen surface (Hydrogen webview tab or+pup browser window) navigates to that URL — same plumbing, no port+mappings, no clip-size limits, no base64 URL bloat.++**Surface (Hydrogen vs pup).** Picked from this precedence chain:++```+--surface hydrogen|pup   →   $ADOM_TTS_SURFACE   →   ~/.adom/tts/config.toml   →   "hydrogen" (default)+```++Pin a default in `~/.adom/tts/config.toml`:++```toml+[play]+surface     = "pup"        # or "hydrogen"+pup_session = "adom-tts"+pup_profile = "adom-tts"+```++Default is `hydrogen` (always available inside the container; pup needs+a desktop Chrome session). Per the pup skill's "pick ONE surface" rule,+there is intentionally no `both` option.++**Tab name.** Hydrogen tabs are titled `"TTS Player"` (distinct from the+playground's `"TTS Playground"`). Repeat `play` calls open new tabs;+close stale ones with `adom-cli hydrogen workspace close-panel <id>`.++**For AIs.** When the user says "read me X", "play this", "make audio+and play it" — go straight to `adom-tts say "X" --out /tmp/x.mp3 --play`.+Don't write an autoplay HTML file. Don't create a port mapping. The+detached child handles the surface and self-shuts down on the audio's+`ended` event (or after a 5-minute cap).++## Install (Tier B via wiki)++```bash+adom-wiki pkg install adom/adom-tts   # installs the CLI + skills+adom-tts install+adom-tts health+```++## Who should use this (and how)++- **demo-recording** — replace `edge-tts --voice ...` invocations with+  `adom-tts say`. Cache hit + phonetic-correct Adom terms for free.+- **any future tour/walkthrough/voiceover** — same pattern: `adom-tts say`,+  don't invoke edge-tts directly.++## Why not invoke edge-tts directly?++Because:++- Every consumer has to know every pronunciation override (or get them+  wrong). Server-side application fixes this once.+- Repeat renders cost real time (+ Microsoft endpoint bandwidth). A+  source-hash cache makes the 2nd + N-th call free.+- When Microsoft changes the edge-tts endpoint (they have; will again),+  every consumer breaks in parallel. One server-side swap fixes everyone.+- Voice consistency: Andrew Neural is approved once in the `Adom` context;+  every call through adom-tts gets that default.++## Related skills++- [tts-pronunciation](../tts-pronunciation/SKILL.md) — the canonical+  phonetic-override table (loaded by service-tts at startup).+- [demo-recording](../demo-recording/SKILL.md) — top consumer.+- [service-kicad](../service-kicad/SKILL.md),+  [step2glb](../step2glb/SKILL.md) — sibling Tier B services + CLIs.++Canonical source: `adom-inc/service-tts` repo + `adom-inc/adom-tts` repo.++## Canonical voice: en-US-AndrewNeural++The standard voice for every Adom demo, walkthrough, or narration is `en-US-AndrewNeural`. Use it as the default in `adom-tts say` unless the user explicitly asks for variety. It's been confirmed on the aci feature-tour demo and several walkthrough recordings as the right balance of clarity, neutrality, and warmth.++## Skip ElevenLabs premades — pull from Shared Voice Library++The default 21-voice ElevenLabs set sounds like AI-generated YouTube narration. When considering an ElevenLabs voice for a specific project, pull from the **Shared Voice Library** sorted by `cloned_by_count`, biased toward husky/raspy/character/accent voices. Andrew Neural remains the bar to beat.++## Audio playback via pup, not new port mappings++For one-off audio playback (TTS test, narration preview), base64-inline the mp3 into a small auto-play HTML, then `browser_open_window` with a data URL, then force-play with `browser_eval` if Chrome's autoplay policy blocks it. Don't create a new public port mapping just to serve one audio file. Past pattern: `port-add 8888` for a one-time TTS preview, never cleaned up.+--- a/skills/tts-pronunciation/SKILL.md+++ b/skills/tts-pronunciation/SKILL.md@@ -1,119 +1,121 @@-----name: tts-pronunciation-description: Canonical table of TTS pronunciation overrides for Adom tool names, acronyms, and product terms that neural TTS engines (edge-tts, piper, azure, eleven-labs) mispronounce. Use when writing narration for any demo / walkthrough / tour / voiceover — look up each Adom-ish term in the table and spell it phonetically in the narration column. Use BEFORE rendering any TTS audio, not after.-  Trigger words: tts pronunciation, tts pronounce, mispronounce, how do I make TTS say, edge-tts pronounce, narration pronunciation, speak adom-tsci, demo narration phonetics.-  Also fires automatically whenever another skill (demo-recording, video-post, voiceover-over-a-recording) is drafting narration text.------# TTS pronunciation — canonical table--Single source of truth for how to write Adom tool names, acronyms, and product terms in narration so neural TTS engines voice them correctly. Every demo-recording / video-post / voiceover flow should consult this file at script-write time (not at render time — fixing mistakes after mux means re-rendering N clips).--## 🛑 The one rule--**If the TTS engine would read a word as something other than what a listener expects, rewrite it in the narration.** The most common fix is spelling an acronym out as individual letters separated by spaces — `adom-tsci` → `adom t s c i`. Edge-TTS and Piper both read letter-space-letter as individual letter names. No-space gets parsed as a single word and butchered.--Captions and on-screen text always use the real name (`adom-tsci`). Only the narration column gets phoneticized.--## The table--Source-of-truth data file: [`pronunciations.json`](pronunciations.json) (same dir). This table is rendered from that file; update the JSON and this section regenerates on skill install.--| Written in product / caption | Narration spelling (what the TTS sees) | Engines confirmed | Reason |-|---|---|---|---|-| `adom-tsci` | `adom t s c i` | edge-tts, piper | TTS reads `tsci` as "sci" / "ski"; letter-by-letter is the intended reading |-| `tscircuit` | `t s circuit` | edge-tts | "tscircuit" sometimes OK but inconsistent across voices; forcing letters is safe |-| `JLCPCB` | `J L C P C B` | edge-tts, azure | Acronym, always spell out |-| `PCB` | `P C B` | all | Acronym |-| `SMT` / `SMD` | `S M T` / `S M D` | all | Acronym |-| `KiCad` | `kai cad` | edge-tts | Some voices say "kee cad"; `kai cad` is the community pronunciation |-| `MPN` | `M P N` | all | Acronym |-| `BOM` | `B O M` or `bill of materials` | all | "bom" gets read as the word "bomb" by some voices; spelling out is safer |-| `FPGA` | `F P G A` | all | Acronym |-| `I²C` / `I2C` | `I squared C` or `I two C` | all | Superscript/digit both mangled |-| `SPI` | `S P I` | all | Acronym |-| `UART` | `you art` | all | Engines try to read as word (rhymes with "heart") which is wrong |-| `MCU` | `M C U` | all | Acronym |-| `GPIO` | `G P I O` | all | Acronym |-| Version numbers like `1.3.7` | `one point three point seven` | all | `1.3.7` gets read as "one three seven" or mangled decimals; always spell |-| Dates like `2026-04-23` | `April twenty third` (or "April twenty third twenty twenty six") | all | Hyphenated date gets mangled |-| Slugs / UUIDs | don't narrate them; reference as "the container" / "the workspace" | — | Never put a slug/UUID in a TTS stream |--## How to add an entry--Anyone can propose a new entry — the table lives in gallia and deploys via `gallia/install.mjs`. Contribution flow:--1. **Verify the problem.** Render the current pronunciation to confirm it's actually wrong:--    ```bash-    edge-tts --voice en-US-AndrewMultilingualNeural \-      --text "this is <your term> version one" \-      --write-media /tmp/_tts-before.mp3-    # Listen: does the TTS say the term the way a human would?-    ```--2. **Find a spelling that fixes it.** Common patterns:-    - Acronym → individual letters with spaces: `JLCPCB` → `J L C P C B`-    - Word with silent/weird letters → phonetic: `KiCad` → `kai cad`-    - Number pattern → words: `1.3.7` → `one point three point seven`-    - Hyphenated brand name → space-or-phonetic: `adom-tsci` → `adom t s c i`--3. **Verify the fix.** Render with the new spelling:--    ```bash-    edge-tts --voice en-US-AndrewMultilingualNeural \-      --text "this is <your phonetic spelling> version one" \-      --write-media /tmp/_tts-after.mp3-    # Confirm it now reads correctly.-    ```--4. **Add to `pronunciations.json`.** One entry per term:--    ```json-    {-      "written": "JLCPCB",-      "narration": "J L C P C B",-      "engines_confirmed": ["edge-tts", "azure"],-      "reason": "Acronym; reads as garbled single word",-      "added_by": "john",-      "added_date": "2026-04-23"-    }-    ```--5. **Commit + publish.** This skill lives inside the adom-tts repo (skills/tts-pronunciation/). To ship a table change, bump adom-tts's version and publish it per publish-skills/adom-tts-publish — every installer gets the update with the app.--## Engines we've tested against--- `edge-tts` (Microsoft Edge neural) — default on the platform, invoked as `edge-tts --voice <voice> --text <text> --write-media <file>`. Voice table:-    - `en-US-AndrewMultilingualNeural` — default for Adom demos (clear, neutral-male)-    - `en-US-AriaNeural` — polished female-    - `en-US-GuyNeural` — warm neutral-male-- `piper` — local offline TTS, fires on the platform when no network.-- `azure-tts` — via Microsoft Azure Speech SDK.-- `eleven-labs` — premium quality, used for hero demos.--Each engine has slightly different parsing rules. If a spelling works in one but not another, add a per-engine note in the `pronunciations.json` entry and pick the most permissive spelling (or provide per-engine overrides).--## Integration — skills that consult this table--The following skills (should) look up terms here before drafting narration. If you spot drift between their embedded tables and this one, this file wins:--- `demo-recording` — Step 2 (write the demo script with narration column)-- `video-post` — storyboard review phase, narration validation-- `voiceover` (when that flow ships) — pre-render check-- Any custom narration script a user writes--## One-liner verify in your own narration--Before rendering a whole 13-clip tour, pipe each scene's narration column through a verifier:--```bash-for narration in "$(jq -r '.scenes[].narration' demo/<name>-demo-script.json)"; do-  tmp=$(mktemp --suffix=.mp3)-  edge-tts --voice en-US-AndrewMultilingualNeural --text "$narration" --write-media "$tmp"-  echo "play: $tmp"-  # Listen or pipe to ffprobe for duration check-done-```--Cheap pre-flight; catches pronunciation and duration issues before you burn render time.+---+name: tts-pronunciation+description: Canonical table of TTS pronunciation overrides for Adom tool names, acronyms, and product terms that neural TTS engines (edge-tts, piper, azure, eleven-labs) mispronounce. Use when writing narration for any demo / walkthrough / tour / voiceover — look up each Adom-ish term in the table and spell it phonetically in the narration column. Use BEFORE rendering any TTS audio, not after.+  Trigger words: tts pronunciation, tts pronounce, mispronounce, how do I make TTS say, edge-tts pronounce, narration pronunciation, speak adom-tsci, demo narration phonetics.+  Also fires automatically whenever another skill (demo-recording, video-post, voiceover-over-a-recording) is drafting narration text.+---++# TTS pronunciation — canonical table++Single source of truth for how to write Adom tool names, acronyms, and product terms in narration so neural TTS engines voice them correctly. Every demo-recording / video-post / voiceover flow should consult this file at script-write time (not at render time — fixing mistakes after mux means re-rendering N clips).++## 🛑 The one rule++**If the TTS engine would read a word as something other than what a listener expects, rewrite it in the narration.** The most common fix is spelling an acronym out as individual letters separated by spaces — `adom-tsci` → `adom t s c i`. Edge-TTS and Piper both read letter-space-letter as individual letter names. No-space gets parsed as a single word and butchered.++Captions and on-screen text always use the real name (`adom-tsci`). Only the narration column gets phoneticized.++## The table++Source-of-truth data file: [`pronunciations.json`](pronunciations.json) (same dir). This table is rendered from that file; update the JSON and this section regenerates on skill install.++| Written in product / caption | Narration spelling (what the TTS sees) | Engines confirmed | Reason |+|---|---|---|---|+| `adom-tsci` | `adom t s c i` | edge-tts, piper | TTS reads `tsci` as "sci" / "ski"; letter-by-letter is the intended reading |+| `tscircuit` | `t s circuit` | edge-tts | "tscircuit" sometimes OK but inconsistent across voices; forcing letters is safe |+| `JLCPCB` | `J L C P C B` | edge-tts, azure | Acronym, always spell out |+| `PCB` | `P C B` | all | Acronym |+| `SMT` / `SMD` | `S M T` / `S M D` | all | Acronym |+| `KiCad` | `kai cad` | edge-tts | Some voices say "kee cad"; `kai cad` is the community pronunciation |+| `MPN` | `M P N` | all | Acronym |+| `BOM` | `B O M` or `bill of materials` | all | "bom" gets read as the word "bomb" by some voices; spelling out is safer |+| `FPGA` | `F P G A` | all | Acronym |+| `I²C` / `I2C` | `I squared C` or `I two C` | all | Superscript/digit both mangled |+| `SPI` | `S P I` | all | Acronym |+| `UART` | `you art` | all | Engines try to read as word (rhymes with "heart") which is wrong |+| `MCU` | `M C U` | all | Acronym |+| `GPIO` | `G P I O` | all | Acronym |+| Version numbers like `1.3.7` | `one point three point seven` | all | `1.3.7` gets read as "one three seven" or mangled decimals; always spell |+| Dates like `2026-04-23` | `April twenty third` (or "April twenty third twenty twenty six") | all | Hyphenated date gets mangled |+| Slugs / UUIDs | don't narrate them; reference as "the container" / "the workspace" | — | Never put a slug/UUID in a TTS stream |++## How to add an entry++Anyone can propose a new entry — the table ships with the `adom/adom-tts` wiki page and installs via `adom-wiki pkg install adom/adom-tts`. Contribution flow:++1. **Verify the problem.** Render the current pronunciation to confirm it's actually wrong:++    ```bash+    edge-tts --voice en-US-AndrewMultilingualNeural \+      --text "this is <your term> version one" \+      --write-media /tmp/_tts-before.mp3+    # Listen: does the TTS say the term the way a human would?+    ```++2. **Find a spelling that fixes it.** Common patterns:+    - Acronym → individual letters with spaces: `JLCPCB` → `J L C P C B`+    - Word with silent/weird letters → phonetic: `KiCad` → `kai cad`+    - Number pattern → words: `1.3.7` → `one point three point seven`+    - Hyphenated brand name → space-or-phonetic: `adom-tsci` → `adom t s c i`++3. **Verify the fix.** Render with the new spelling:++    ```bash+    edge-tts --voice en-US-AndrewMultilingualNeural \+      --text "this is <your phonetic spelling> version one" \+      --write-media /tmp/_tts-after.mp3+    # Confirm it now reads correctly.+    ```++4. **Add to `pronunciations.json`.** One entry per term:++    ```json+    {+      "written": "JLCPCB",+      "narration": "J L C P C B",+      "engines_confirmed": ["edge-tts", "azure"],+      "reason": "Acronym; reads as garbled single word",+      "added_by": "john",+      "added_date": "2026-04-23"+    }+    ```++5. **Commit + publish.** This skill lives inside the adom-tts repo (skills/tts-pronunciation/). To ship a table change, bump adom-tts's version and publish it per publish-skills/adom-tts-publish — every installer gets the update with the app.++## Engines we've tested against++- `edge-tts` (Microsoft Edge neural) — default on the platform, invoked as `edge-tts --voice <voice> --text <text> --write-media <file>`. Voice table:+    - `en-US-AndrewMultilingualNeural` — default for Adom demos (clear, neutral-male)+    - `en-US-AriaNeural` — polished female+    - `en-US-GuyNeural` — warm neutral-male+- `piper` — local offline TTS, fires on the platform when no network.+- `azure-tts` — via Microsoft Azure Speech SDK.+- `eleven-labs` — premium quality, used for hero demos.++Each engine has slightly different parsing rules. If a spelling works in one but not another, add a per-engine note in the `pronunciations.json` entry and pick the most permissive spelling (or provide per-engine overrides).++## Integration — skills that consult this table++The following skills (should) look up terms here before drafting narration. If you spot drift between their embedded tables and this one, this file wins:++- `demo-recording` — Step 2 (write the demo script with narration column)+- `video-post` — storyboard review phase, narration validation+- `voiceover` (when that flow ships) — pre-render check+- Any custom narration script a user writes++## One-liner verify in your own narration++Before rendering a whole 13-clip tour, pipe each scene's narration column through a verifier:++```bash+for narration in "$(jq -r '.scenes[].narration' demo/<name>-demo-script.json)"; do+  tmp=$(mktemp --suffix=.mp3)+  edge-tts --voice en-US-AndrewMultilingualNeural --text "$narration" --write-media "$tmp"+  echo "play: $tmp"+  # Listen or pipe to ffprobe for duration check+done+```++Cheap pre-flight; catches pronunciation and duration issues before you burn render time.++

Comments

No comments yet.

Log in to comment.