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
Publish 1.9.49
5 files changed
+118−66
README.md+21−6@@ -40,13 +40,16 @@ tour/walkthrough tool route through it. ### The live player -One shared player for all your AI threads. The header tells you WHO is speaking (the-thread) and WHAT the clip is about. Here the `flight-search` thread is reading out-flight options. The transcript highlights word-by-word as it speaks, the version badge-shows which build is running, and the green light breathes while the AI's pipe to the-player is live.+One shared player for all your AI threads, built on the Adom dashboard contract: the+house header names WHO is speaking (the thread) and WHAT the clip is about, the starter+panel teaches you what to say to your AI (copy a phrase or send it straight to a new+thread, or play the built-in demo), and the status pill row shows the player, the+playback surface, the voice service and the queue at a glance (tap a pill for the+detail, and a red pill always names the fix). The transcript highlights word by word as+it speaks, and the freshness pill under the transport tells you when the audio was made,+so an old clip can never pass as news. -+ ### The replayable History @@ -66,6 +69,13 @@ of `reply.mp3` entries helps no one.  +### In the Hydrogen dock++Adom TTS is a dock app. Add it from Dock settings (the gear at the top of the rail) with+`adom/adom-tts`: the card installs the CLI, starts the player and opens it in a webview+tab named Adom TTS. The gear in the player's header opens Settings, where you pick+whether clips play in that webview tab or in a Pup desktop browser window.+ ## Install ```bash@@ -159,6 +169,11 @@ Paste any of these into Claude Code; they use the exact words the skill triggers | `adom-tts play last\|<id>\|<path>` | Replay a clip (newest, by id/hash, or a file). | | `adom-tts control replay\|pause\|play\|toggle` | Drive the live player without reloading it. | | `adom-tts reopen` | Re-surface the player if its window was closed (queue survives). |+| `adom-tts open [--print-url]` | Show the player (start one if needed). `--print-url` is the dock's serve step: prints the direct URL and exits. |+| `adom-tts settings [set surface webview\|pup]` | Show or change persistent settings (same values as the gear in the player). |+| `adom-tts ui toast "<text>" [--kind info\|success\|error]` | Raise a toast on the player page (the AI narrating over the app). |+| `adom-tts demo` | Play the built-in three-marker demo (what the starter panel's Play the demo button runs). |+| `adom-tts load` / `cue` / `cue-status` / `queue clear\|drop` / `control skip` | Live-demo markers and queue management (see the user skill). | | `adom-tts logs [--tail N]` | Playback/synthesis event log with verdicts + anomaly hints. | | `adom-tts serve [--port N]` | Open the Playground webview. | | `adom-tts voices` / `pronunciations` / `health` / `config` | Service info. |
bin/adom-tts⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-tts",- "version": "1.9.48",+ "version": "1.9.49", "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+22−1@@ -4,7 +4,7 @@ "slug": "adom-tts", "title": "Adom TTS: Text to Speech", "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.48",+ "version": "1.9.49", "license": "MIT", "tags": [ "tts",@@ -12,6 +12,7 @@ "audio", "demo", "accessibility",+ "cli", "open-source", "mit" ],@@ -64,5 +65,25 @@ ], "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]" } }
skills/adom-tts/SKILL.md+74−58@@ -1,7 +1,7 @@ --- name: adom-tts description: >- Thin Rust CLI for service-tts — Adom's shared edge-tts wrapper with+ 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,@@ -14,7 +14,7 @@ description: > 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+ 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",@@ -25,12 +25,12 @@ description: > "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+ 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, 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`@@ -51,20 +51,20 @@ line); it is an internal host, not something a caller ever opens by hand. ## 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:+**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:+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>" \+ 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+ 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. **If it does not play, YOU can drive the player — this is not human-only.**+4. **If it does not play, YOU can drive the player, this is not human-only.** `adom-tts status`; if it reports PAUSED, resume it yourself with `adom-tts control play` (also `control pause|replay|toggle`). A paused queue holds every thread's clips, and waiting will never clear it. A thread once@@ -78,20 +78,20 @@ line); it is an internal host, not something a caller ever opens by hand. tells you roughly how many seconds of audio are queued ahead of yours. 5. 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.+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.+# 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+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+# 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 @@ -101,10 +101,10 @@ 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).+# Synthesize without playing (rare, only when you genuinely just need the file). adom-tts say "Hello from adom-tsci" --out hello.mp3 -# Speak a FILE's contents — @file is first-class (curl-style). A bare existing+# Speak a FILE's contents, @file is first-class (curl-style). A bare existing # path also works (with a notice). A path that doesn't exist is a hard error: # the CLI will NEVER read a path aloud ("slash tmp slash...") to the human. adom-tts say @/tmp/notes.txt --out notes.mp3 --thread "<t>" --desc "<what it says>" --play --confirm@@ -116,7 +116,7 @@ cat narration.txt | adom-tts say - --out narration.mp3 --play 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).+# Raw text (no pronunciation overrides, rare, for non-Adom narration). adom-tts say "Generic sentence" --out g.mp3 --no-pronunciations --play ``` @@ -151,33 +151,33 @@ loaded more than 6 hours ago are no longer prefetched: reload before an old demo. **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 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. **Default to the hydrogen webview surface. Only use `--surface pup` when the user explicitly asks for it.** The default (no `--surface`) is the hydrogen-webview and it is the reliable path — proven end-to-end. Pup is more fragile:+webview and it is the reliable path, proven end-to-end. Pup is more fragile: with more than one Bridge machine (ab, formerly "Adom Desktop") on the relay its window is ambiguous and can't auto-open (the tool tells you which desktops it sees). So do NOT auto-route a "pup window_mode" user into `--surface pup`; just narrate on the default surface. If you DO need pup (user asked), first `export ADOM_BRIDGE_TARGET=<host>`-(the desktop the human is at). The tool self-surfaces — you NEVER hand-open a+(the desktop the human is at). The tool self-surfaces, you NEVER hand-open a URL: the drainer opens the player itself, and `adom-tts reopen` re-surfaces the SAME surface (a pup drainer re-opens a pup window, hydrogen re-offers its tab), both bound to the live player port. If a clip still comes back `surface-unavailable`, the message NAMES the live player-page URL and says to-re-send WITHOUT `--surface pup` — do that; hydrogen is reliable.+re-send WITHOUT `--surface pup`, do that; hydrogen is reliable. -**Two different URLs — never confuse them.** `adom-tts config` prints a-`service url` (the TTS API, where audio is synthesized — NOT a page to open) and-a `player page` (the live proxy URL at the current port — THIS is what shows the+**Two different URLs, never confuse them.** `adom-tts config` prints a+`service url` (the TTS API, where audio is synthesized, NOT a page to open) and+a `player page` (the live proxy URL at the current port, THIS is what shows the player). Opening the service URL shows nothing; if you ever need to eyeball the player, use the `player page` line. **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+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.@@ -185,10 +185,10 @@ 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 —+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+ 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),@@ -198,7 +198,7 @@ neither should you. Installing one: Unreachable service = report it, full stop. No local install, ever. -`say` emits `→ <path> (<bytes>, cache=hit|miss, hash=<prefix>)` — scripts+`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.@@ -220,24 +220,24 @@ 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 —+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+**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+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.+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`+**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@@ -246,47 +246,47 @@ adom-tts check x.mp3 # non-blocking: prints QUEUED / PLAYING / PLAYED / FAIL adom-tts status # broker view: is a player live, what's playing now, whole queue in order ``` **Resends are refused (exit 7).** Identical audio already queued or played in-the last 15 min will NOT queue again — a resend always double-plays on the+the last 15 min will NOT queue again, a resend always double-plays on the human. If playback seems slow, the clip is buffered and auto-retried; poll `adom-tts check <clip>` instead of re-sending. `--allow-repeat` exists only for a human explicitly asking to hear something twice. -For hands-free / driving, **`--confirm` is THE canonical path** — it is reliable+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+(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+(`~/.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+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).+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+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.+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+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@@ -308,7 +308,7 @@ competing for the shared player. The player header also shows a **live-link LED* ## Drive mode (hands-free auto-player) -You wrote an mp3 — now play it. Don't author a custom autoplay HTML.+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.@@ -321,7 +321,7 @@ adom-tts play last adom-tts play 20260506-104555-c55f79f0 adom-tts play 5f79f0 -# Block until playback finishes (--wait, max 5 min) — useful for shell scripts+# 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 ```@@ -330,7 +330,7 @@ adom-tts play /tmp/x.mp3 --wait 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+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:@@ -352,12 +352,28 @@ 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>`.+**Tab name.** The player's Hydrogen tab is titled `"Adom TTS"` (the same+title the dock uses, so a dock click and the drainer share ONE tab). Every+clip from every thread lands in that one tab; nothing opens a second one.++**Settings.** The gear in the player header opens Settings (Adom standard word:+settings, never preferences). It writes the same file as the CLI:+`adom-tts settings` shows it, `adom-tts settings set surface webview|pup`+changes the default surface. A change takes effect on the next player spawn+(`adom-tts restart` when nothing is playing).++**The dock and the page.** `adom-tts open` shows the player (starts one if none+is live) and returns. `adom-tts open --print-url` is the Hydrogen dock's serve+step (prints the direct loopback URL, exits). `adom-tts ui toast "<text>"+[--kind info|success|error]` raises a toast on the page, which is how you+narrate over the running app during a test or a demo ("running step 3 of 8").+`adom-tts demo` plays the built-in three-marker demo, the same thing the+starter panel's "Play the demo" button runs. The status pill row on the page+(player, surface, voice service, queue) is the same truth `/api/health`+serves to the dock card; a red pill's detail names the exact next command. **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`.+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).@@ -374,9 +390,9 @@ adom-tts health ## Who should use this (and how) -- **demo-recording** — replace `edge-tts --voice ...` invocations with+- **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`,+- **any future tour/walkthrough/voiceover**, same pattern: `adom-tts say`, don't invoke edge-tts directly. ## Why not invoke edge-tts directly?@@ -394,11 +410,11 @@ Because: ## Related skills -- [tts-pronunciation](../tts-pronunciation/SKILL.md) — the canonical+- [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.+- [demo-recording](../demo-recording/SKILL.md), top consumer. - [service-kicad](../service-kicad/SKILL.md),- [step2glb](../step2glb/SKILL.md) — sibling Tier B services + CLIs.+ [step2glb](../step2glb/SKILL.md), sibling Tier B services + CLIs. Canonical source: `adom-inc/service-tts` repo + `adom-inc/adom-tts` repo. @@ -406,7 +422,7 @@ Canonical source: `adom-inc/service-tts` repo + `adom-inc/adom-tts` repo. 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+## 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.