← Commit history
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-tts"-version = "1.9.20"+version = "1.9.21" edition = "2021" license = "AGPL-3.0-or-later" repository = "https://github.com/adom-inc/adom-tts"
VERSION+1−1
@@ -1 +1 @@-1.9.20+1.9.21
bin/adom-tts
⋯ 1 unchanged line ⋯
demo/adom-tts-demo-script.mdadded+53
@@ -0,0 +1,53 @@+# Adom TTS — Text to Speech — wiki walkthrough video++**Target duration:** 0:50–1:00 (4 clips)+**Recorded:** 2026-07-04+**Final output:** `~/project/recordings/adom-tts-walkthrough.webm`+**Wiki target:** `adom/adom-tts` (README embed + page video asset)+**Hero frame:** taken from scene 1 (player mid-karaoke)+**App version demoed:** 1.9.20++## Dependencies++- `adom-tts` 1.9.20 (narration TTS — the app narrates its own demo)+- puppeteer-core 24.x + Chrome for Testing (in-container screencast; the+  player UI is deterministic HTML so scenes are staged states of the real+  `src/player.html`, driven by scripted JS — no live audio plays while+  recording)+- `video-post` 1.0.3, `ffmpeg 6+`++## Recording config++- **Voice:** `en-US-AndrewNeural` via `adom-tts say` (house voice, pronunciation+  overrides server-side)+- **Surface:** in-container headless Chrome, `page.screencast` at 900x620.+  Every scene keeps continuous animation (karaoke advance, typing, LED+  breathing) so frames flow.+- **Captions:** rendered inside the page (small chip, top-center) — no+  Hydrogen caption tooling touched (user's live workspace stays untouched).++## Scenes++| # | id | Caption (on-screen) | Narration (TTS) | Target | Choreography |+|---|---|---|---|---|---|+| 1 | player | `One shared player for all your AI threads` | `This is Adom TTS. Your AI speaks its answers out loud through one shared player. The header shows which thread is talking and what the update is about, and the transcript follows along word by word.` | 14s | player page, clip "flight-search" playing; karaoke advances a word every 400ms; LED breathing; seek bar creeps |+| 2 | verify | `Playback is verified, never assumed` | `Every clip is verified. The A I must name its thread and describe the update, and confirm blocks until the audio truly reached your ears. If your connection drops, clips buffer and retry on their own.` | 13s | terminal page: the say command types out, ACCEPTED + CONFIRMED lines appear in sequence |+| 3 | history | `Missed a clip? It waits for you in History` | `Missed something? Every clip lands in the history with who sent it and what it says. Clips you did not fully hear are flagged. Tap replay and it plays immediately — your tap outranks the machine queue.` | 15s | player + history drawer; missed row pulses; replay button flashes; header swaps to "(replay)" and karaoke restarts |+| 4 | outro | `adom-wiki pkg install adom/adom-tts` | `Adom TTS is open source under M I T, on the Adom wiki. One install command, and your A I has a voice.` | 9s | hero image slow zoom; install command types under it |++## Reproduction++```bash+cd /home/adom/project/adom-tts+python3 demo/build_scenes.py          # writes staged scene HTML to /tmp/ttsdemo/scenes+node demo/record.mjs                  # screencasts each scene to /tmp/ttsdemo/raw+bash demo/tts_and_mux.sh              # adom-tts say per scene + ffmpeg mux -> narrated clips + manifest+video-post storyboard /tmp/ttsdemo/manifest.json   # MANDATORY user review+bash demo/finalize.sh                 # concat + keyframes + hero + wiki upload + README embed+```++## Known issues from this rev++- Scenes are staged states of the real player HTML (deterministic, no live+  audio out) rather than a capture of the live Hydrogen tab — chosen so the+  user's active workspace and speakers are untouched during recording.
demo/build_scenes.pyadded+127
@@ -0,0 +1,127 @@+#!/usr/bin/env python3+"""Stage the 4 demo scenes as self-contained HTML (states of the real player UI)."""+import os, json, pathlib++ROOT = pathlib.Path(__file__).resolve().parent.parent+OUT = pathlib.Path('/tmp/ttsdemo/scenes'); OUT.mkdir(parents=True, exist_ok=True)+player = (ROOT / 'src/player.html').read_text().replace('{{VERSION}}', '1.9.20')++CAPTION_CSS = """+<style>.demo-cap{position:fixed;top:52px;left:50%;transform:translateX(-50%);z-index:99;+background:rgba(13,17,23,.92);border:1px solid #2b3a52;border-radius:999px;padding:6px 18px;+font:600 13.5px 'Familjen Grotesk',system-ui,sans-serif;color:#7fe3dd;box-shadow:0 6px 24px rgba(0,0,0,.5)}</style>+"""++def cap(text):+    return CAPTION_CSS + f'<div class="demo-cap">{text}</div>'++WORDS1 = ("This is the flight-search thread. Round trip to Denver looks best on Tuesday: "+          "two forty five nonstop, four hundred twelve dollars on United. Want me to hold it?")++# ---- scene 1: player mid-karaoke ----+inj1 = cap("One shared player for all your AI threads") + """+<script>window.addEventListener('load',function(){+var words=%s.split(" ");+try{applyClip({id:1,title:'flight-answer.mp3',meta:'en-US-AndrewNeural',label:'flight-search',+  desc:'Denver flight options — best is Tue 2:45pm nonstop $412',words:words});}catch(e){}+var a=document.getElementById('a'); a.dispatchEvent(new Event('playing'));+var old=document.getElementById('live'); if(old){var neo=old.cloneNode(false);neo.className='led ok';neo.id='live2';old.parentNode.replaceChild(neo,old);}+var di=document.getElementById('diag'); if(di)di.remove();+var spans=document.querySelectorAll('.transcript w'); var i=0; var total=16.0; var t=0;+setInterval(function(){+  var s=document.getElementById('status'); if(s){s.textContent='playing';s.className='pill playing';}+  document.body.classList.add('is-playing');+  var pi=document.querySelector('#playicon path'); if(pi)pi.setAttribute('d','M6 5h4v14H6zM14 5h4v14h-4z');+},80);+setInterval(function(){+  if(i<spans.length){ spans.forEach(function(el,k){el.classList.toggle('active',k===i);el.classList.toggle('spoken',k<i);}); i++; }+  t=Math.min(total,t+0.42);+  var c=document.getElementById('cur'); if(c)c.textContent='0:'+String(Math.floor(t)).padStart(2,'0');+  var d=document.getElementById('dur'); if(d)d.textContent='0:16';+  var sk=document.getElementById('seek'); if(sk)sk.value=Math.round(t/total*1000);+},420);+});</script>""" % json.dumps(WORDS1)+(OUT / 'player.html').write_text(player.replace('</body>', inj1 + '</body>'))++# ---- scene 3: history + replay preempt ----+inj3 = cap("Missed a clip? It waits for you in History") + """+<script>window.addEventListener('load',function(){+var words="Missed clips wait for you right here in the history, one tap from playing again.".split(" ");+try{applyClip({id:1,title:'reply.mp3',meta:'en-US-AndrewNeural',label:'Adom-TTS',desc:'how the replayable history works',words:words});}catch(e){}+var now=Date.now();+renderHistory([+ {seq:1,ts_ms:now-35000,label:'Adom-TTS',desc:'how the replayable history works',verdict:'ok',dur_s:11.2},+ {seq:2,ts_ms:now-160000,label:'flight-search',desc:'Denver flight options — best is Tue 2:45pm nonstop $412',verdict:'ok',dur_s:16.4},+ {seq:3,ts_ms:now-380000,label:'deploy-watch',desc:'nightly deploy finished, zero errors',verdict:'no-playback',dur_s:24.0},+ {seq:4,ts_ms:now-1500000,label:'login-bug',desc:'login fix verified on prod',verdict:'ok',dur_s:19.8}+], now);+var a=document.getElementById('a'); a.dispatchEvent(new Event('playing'));+var old=document.getElementById('live'); if(old){var neo=old.cloneNode(false);neo.className='led ok';neo.id='live2';old.parentNode.replaceChild(neo,old);}+var di=document.getElementById('diag'); if(di)di.remove();+setInterval(function(){var s=document.getElementById('status'); if(s){s.textContent='playing';s.className='pill playing';} document.body.classList.add('is-playing');+  var pi=document.querySelector('#playicon path'); if(pi)pi.setAttribute('d','M6 5h4v14H6zM14 5h4v14h-4z');},80);+// beat: 3s in, pulse the missed row; 6s in, flash its replay button; 8s, swap to replay+setTimeout(function(){var rows=document.querySelectorAll('.hist-row'); if(rows[2]){rows[2].style.transition='background .4s';rows[2].style.background='rgba(212,160,23,.12)';}},3000);+setTimeout(function(){var b=document.querySelectorAll('.hist-replay')[2]; if(b){b.style.transition='transform .25s, background .25s';b.style.background='rgba(0,184,177,.35)';b.style.transform='scale(1.35)';setTimeout(function(){b.style.transform='scale(1)';b.textContent='✓';},450);}},6000);+setTimeout(function(){+  var w2="nightly deploy finished, zero errors, all services healthy after the rollout.".split(" ");+  try{applyClip({id:2,title:'deploy-note.mp3',meta:'en-US-AndrewNeural',label:'deploy-watch',desc:'(replay) nightly deploy finished, zero errors',words:w2});}catch(e){}+  var a2=document.getElementById('a'); a2.dispatchEvent(new Event('playing'));+  var spans=document.querySelectorAll('.transcript w'); var i=0;+  setInterval(function(){ if(i<spans.length){spans.forEach(function(el,k){el.classList.toggle('active',k===i);el.classList.toggle('spoken',k<i);}); i++;} },430);+},8200);+});</script>"""+(OUT / 'history.html').write_text(player.replace('</body>', inj3 + '</body>'))++# ---- scene 2: verified send (terminal typing) ----+term = """<!doctype html><html><head><style>+body{margin:0;background:#0d1117;display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:ui-monospace,Menlo,monospace}+.term{width:820px;background:#0a0e14;border:1px solid #2b3a52;border-radius:12px;box-shadow:0 18px 50px rgba(0,0,0,.55);overflow:hidden}+.bar{height:34px;background:#161b22;border-bottom:1px solid #2b3a52;display:flex;align-items:center;padding:0 12px;color:#8b98a5;font-size:12px}+.bar .dot{width:10px;height:10px;border-radius:50%;background:#2b3a52;margin-right:6px}+pre{margin:0;padding:18px 20px;font-size:14px;line-height:1.7;color:#e6edf3;white-space:pre-wrap;min-height:330px}+.p{color:#7fe3dd}.c{color:#8b98a5}.g{color:#3fb950}+.cursor{display:inline-block;width:8px;height:16px;background:#7fe3dd;vertical-align:-2px;animation:bl 1s steps(1) infinite}+@keyframes bl{50%{opacity:0}}+CAPCSS</style></head><body>+CAPDIV+<div class="term"><div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span>claude — adom-tts</div><pre id="t"><span class="p">$</span> <span id="cmd"></span><span class="cursor" id="cur"></span><span id="out"></span></pre></div>+<script>+var CMD = 'adom-tts say "This is the flight-search thread — best Denver option is Tuesday 2:45pm nonstop." \\\\\\n    --thread "flight-search" --desc "Denver flight options" \\\\\\n    --out /tmp/answer.mp3 --play --confirm';+var i=0;+var typer=setInterval(function(){+  if(i<CMD.length){ document.getElementById('cmd').textContent=CMD.slice(0,++i); }+  else clearInterval(typer);+},34);+var OUT=[+ [5200,'\\n\\n→ /tmp/answer.mp3 (98304 bytes, cache=miss, hash=a1b2c3d4e5f6)'],+ [6100,'\\n<span class="c"># ACCEPTED "answer.mp3" — next up in the shared player.</span>'],+ [7000,'\\n<span class="c"># to KNOW it played: adom-tts check answer.mp3 — or --confirm blocks until heard.</span>'],+ [10300,'\\n\\n<span class="g">✓ CONFIRMED: "answer.mp3" played fully to the human (16/16s)</span>']+];+OUT.forEach(function(o){ setTimeout(function(){ document.getElementById('out').innerHTML+=o[1]; document.getElementById('cur').style.display='none'; }, o[0]); });+</script></body></html>"""+term = term.replace('CAPCSS', CAPTION_CSS.replace('<style>','').replace('</style>',''))+term = term.replace('CAPDIV', '<div class="demo-cap">Playback is verified, never assumed</div>')+(OUT / 'verify.html').write_text(term)++# ---- scene 4: outro (hero slow zoom + install typing) ----+hero_path = ROOT / 'screenshots/hero-v3.png'+outro = """<!doctype html><html><head><style>+body{margin:0;background:#0d1117;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;font-family:ui-monospace,Menlo,monospace}+.wrap{width:860px;border-radius:14px;overflow:hidden;border:1px solid #2b3a52;box-shadow:0 18px 60px rgba(0,0,0,.6)}+img{width:100%%;display:block;animation:zoom 14s linear forwards}+@keyframes zoom{from{transform:scale(1)}to{transform:scale(1.07)}}+.inst{margin-top:26px;font-size:16px;color:#7fe3dd;background:#0a0e14;border:1px solid #2b3a52;border-radius:10px;padding:12px 22px}+.cursor{display:inline-block;width:8px;height:16px;background:#7fe3dd;vertical-align:-2px;animation:bl 1s steps(1) infinite}+@keyframes bl{50%%{opacity:0}}+</style></head><body>+<div class="wrap"><img src="file://%s"></div>+<div class="inst"><span id="cmd"></span><span class="cursor"></span></div>+<script>+var CMD='adom-wiki pkg install adom/adom-tts'; var i=0;+setTimeout(function(){ var t=setInterval(function(){ if(i<CMD.length){document.getElementById('cmd').textContent=CMD.slice(0,++i);} else clearInterval(t); },50); },1500);+</script></body></html>""" % str(hero_path)+(OUT / 'outro.html').write_text(outro)++print("scenes:", sorted(p.name for p in OUT.iterdir()))
demo/record.mjsadded+26
@@ -0,0 +1,26 @@+// Screencast each staged scene to webm (real-time, in-container).+import puppeteer from '/home/adom/project/node_modules/puppeteer-core/lib/esm/puppeteer/puppeteer-core.js';+const CHROME = process.env.CHROME || '/home/adom/.cache/puppeteer/chrome/linux-146.0.7680.76/chrome-linux64/chrome';+const SCENES = [+  { id: 'player',  secs: 15 },+  { id: 'verify',  secs: 14 },+  { id: 'history', secs: 16 },+  { id: 'outro',   secs: 10 },+];+const b = await puppeteer.launch({ executablePath: CHROME, headless: 'new',+  args: ['--no-sandbox','--disable-gpu','--hide-scrollbars','--font-render-hinting=none',+         '--disable-features=CalculateNativeWinOcclusion','--disable-backgrounding-occluded-windows',+         '--disable-renderer-backgrounding','--disable-background-timer-throttling','--force-color-profile=srgb'] });+for (const s of SCENES) {+  const p = await b.newPage();+  await p.setViewport({ width: 900, height: 620, deviceScaleFactor: 2 });+  await p.goto(`file:///tmp/ttsdemo/scenes/${s.id}.html`, { waitUntil: 'networkidle0', timeout: 10000 }).catch(()=>{});+  await new Promise(r => setTimeout(r, 600));   // settle fonts/first paint+  const rec = await p.screencast({ path: `/tmp/ttsdemo/raw/${s.id}.webm` });+  await new Promise(r => setTimeout(r, s.secs * 1000));+  await rec.stop();+  console.log('recorded', s.id, s.secs + 's');+  await p.close();+}+await b.close();+console.log('done');
install.sh+1−1
@@ -14,7 +14,7 @@ echo "Installed adom-tts -> $BIN_DIR/adom-tts"  # 2) Claude Code skill (user tier only — dev/publish skills stay in source). SKILL_DIR="${HOME}/.claude/skills"-for s in adom-tts; do+for s in adom-tts tts-pronunciation; do   if [ -f "$DIR/skills/$s/SKILL.md" ]; then     mkdir -p "$SKILL_DIR/$s"     install -m 0644 "$DIR/skills/$s/SKILL.md" "$SKILL_DIR/$s/SKILL.md"
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-tts",-  "version": "1.9.20",+  "version": "1.9.21",   "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+14−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.20",+  "version": "1.9.21",   "license": "MIT",   "tags": [     "tts",@@ -12,6 +12,7 @@     "audio",     "demo",     "accessibility",+    "cli",     "open-source",     "mit"   ],@@ -64,5 +65,17 @@   ],   "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
skills/tts-pronunciation/SKILL.mdadded+120
@@ -0,0 +1,120 @@+---+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.+
skills/tts-pronunciation/pronunciations.jsonadded+151
@@ -0,0 +1,151 @@+{+  "$schema_comment": "Canonical TTS pronunciation table. Each entry: 'written' is how the term appears in captions/docs/code; 'narration' is the spelling fed to the TTS engine; 'engines_confirmed' lists which engines were tested; 'reason' and 'added_by' are metadata. Consumer skills (demo-recording, video-post, voiceover) look up the 'written' key and substitute 'narration' into any TTS input.",+  "version": "1.0.0",+  "updated": "2026-04-23",+  "entries": [+    {+      "written": "adom-tsci",+      "narration": "adom t s c i",+      "engines_confirmed": ["edge-tts"],+      "reason": "TTS reads 'tsci' as 'sci' or 'ski' — a single word. Spelling each letter with spaces gets the intended four-letter reading.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "tscircuit",+      "narration": "t s circuit",+      "engines_confirmed": ["edge-tts"],+      "reason": "Inconsistent across voices; some render 'tscircuit' correctly, others as 'tuh-circuit' or 'ts-circuit'. Safer to force letters.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "JLCPCB",+      "narration": "J L C P C B",+      "engines_confirmed": ["edge-tts", "azure"],+      "reason": "Acronym — reads as garbled single word without letter separation.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "PCB",+      "narration": "P C B",+      "engines_confirmed": ["edge-tts", "piper", "azure"],+      "reason": "Acronym, always spelled.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "SMT",+      "narration": "S M T",+      "engines_confirmed": ["edge-tts", "piper", "azure"],+      "reason": "Acronym (surface-mount technology).",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "SMD",+      "narration": "S M D",+      "engines_confirmed": ["edge-tts", "piper", "azure"],+      "reason": "Acronym (surface-mount device).",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "KiCad",+      "narration": "kai cad",+      "engines_confirmed": ["edge-tts"],+      "reason": "Community pronunciation is 'kai cad' (long-i); some voices default to 'kee cad'. Forcing the phonetic spelling matches the community norm.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "MPN",+      "narration": "M P N",+      "engines_confirmed": ["edge-tts", "azure"],+      "reason": "Acronym (manufacturer part number).",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "BOM",+      "narration": "bill of materials",+      "engines_confirmed": ["edge-tts", "piper"],+      "reason": "'BOM' reads as the word 'bomb' in many voices. 'B O M' also reads oddly because of the mouth-shape. Expanding to 'bill of materials' is cleanest.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "FPGA",+      "narration": "F P G A",+      "engines_confirmed": ["edge-tts", "piper", "azure"],+      "reason": "Acronym (field-programmable gate array).",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "I²C",+      "narration": "I squared C",+      "engines_confirmed": ["edge-tts"],+      "reason": "Superscript '²' gets dropped or mangled. 'I squared C' is the engineer-standard reading.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "I2C",+      "narration": "I squared C",+      "engines_confirmed": ["edge-tts"],+      "reason": "ASCII-fallback for I²C — same audio reading, match the standard spoken form.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "SPI",+      "narration": "S P I",+      "engines_confirmed": ["edge-tts", "piper"],+      "reason": "Acronym (serial peripheral interface). Engines otherwise try 'spy'.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "UART",+      "narration": "you art",+      "engines_confirmed": ["edge-tts"],+      "reason": "Engineers say 'you-art' (two syllables). Engines tend to rhyme it with 'heart' ('yoo-art' as one syllable). Forcing 'you art' with a space reproduces the intended split.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "MCU",+      "narration": "M C U",+      "engines_confirmed": ["edge-tts", "piper"],+      "reason": "Acronym (microcontroller unit).",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "GPIO",+      "narration": "G P I O",+      "engines_confirmed": ["edge-tts", "piper"],+      "reason": "Acronym (general-purpose input/output). Some voices try 'jee-pee-oh' with a blended i/o; letter-by-letter is cleaner.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "VERSION_NUMBER_PATTERN",+      "narration": "spell as words — e.g. '1.3.7' → 'one point three point seven'",+      "engines_confirmed": ["edge-tts", "piper", "azure"],+      "reason": "Numeric version strings like '1.3.7' get read as 'one three seven' or mangled decimals. Always spell in narration.",+      "added_by": "john",+      "added_date": "2026-04-23"+    },+    {+      "written": "DATE_PATTERN",+      "narration": "spell as words — e.g. '2026-04-23' → 'April twenty third'",+      "engines_confirmed": ["edge-tts", "piper"],+      "reason": "Hyphenated dates mangle. Convert to spoken form.",+      "added_by": "john",+      "added_date": "2026-04-23"+    }+  ]+}