Simple Chrome TTS extension
1.15 (2026-09-24 00:27)
Chrome's manifest version field only accepts dot-separated integers, so it can't hold a
timestamp. The human-readable build stamp lives in manifest.json's version_name field
instead — visible in chrome://extensions and in the popup footer, which reads it live from
the manifest so it can never go stale on its own. Bump both together on every meaningful
change: version gets a normal bump, version_name becomes "<version> (<local date> <local time>)".
Separate from the version/version_name pair above, this repo also carries a
commit-derived build stamp per _agent-commons\reference\build-stamp-standard.md
(🦑 EMBER · 4a1c2f · 2026-09-06 style — emoji + BIP-39 codeword + short SHA + commit date,
optionally prefixed with the manifest version). There is no bundler/build step here, so
nothing regenerates it automatically. Run it by hand after committing a change:
npm run stamp
This writes/overwrites the committed build-stamp.json at the repo root from the current
git rev-parse HEAD (and the manifest.json version field, if set). Commit
build-stamp.json as its own follow-up commit — it necessarily lags one commit behind
whatever change it's stamping, since it can't know its own SHA in advance. The popup reads
it at open time (fetch(chrome.runtime.getURL('build-stamp.json'))) and shows it quietly as
the footer's title attribute (hover to see it) — a fresh checkout before the first
npm run stamp run just has no tooltip, not an error. npm test includes a smoke check
that build-stamp.json, if present, is internally consistent (its stamp matches a fresh
derivation of its own recorded sha/date/version).
- 2026-08-14: Hotkey moved to
Ctrl+Alt+L(suggested default only — existing installs keep whatever's set inchrome://extensions/shortcutsuntil re-bound by hand) for a right-hand-reachable chord. Speech can now be cancelled two new ways, both routing through the samechrome.tts.stop()path the popup's Stop button already used: pressing the read hotkey again while something is already speaking (always cancels — even if a different selection is active, it never starts reading that instead), and pressing Escape, which works from any tab regardless of which one is actually speaking. Full design:docs/superpowers/specs/hotkey-and-cancel.md. - 2026-08-14: "Save as audio clip" moved from the right-click context menu to a popup
button. Right-click now shows a single "Read with GrayTTS" item — no submenu, since
Chrome/Edge only nests an extension's items into a flyout when there are 2 or more
top-level ones. The popup gained a "🎙 Save as audio clip" button (below the Pause/Stop
row) that grabs the active tab's current text selection the same way the hotkey already
does, then runs through the exact same capture flow as before. Selecting nothing before
clicking it shows an inline error in the popup; every other failure (capture already in
progress, picker cancelled, etc.) still uses the existing toolbar badge. Full design:
docs/superpowers/specs/relocate-save-clip-to-popup.md. - 2026-08-14: The bottom-center word overlay (v1.8) is now clickable — clicking it
pauses speech (the word turns red and freezes on the last word shown), clicking again
resumes. Fully in sync with the popup's existing Pause/Resume button in both directions:
pausing via the overlay updates the popup's button label, and pausing via the popup turns
the overlay red too, since both now drive and reflect the same
chrome.ttspause/resume state inbackground.js. No change to the separate in-page CSS Custom Highlight, which already freezes naturally during a pause. Full design:docs/superpowers/specs/clickable-overlay-pause.md. - 2026-08-13: Added "Save as audio clip" — a new right-click context-menu item next to "Read with GrayTTS" that records the spoken selection as a downloaded
.webmfile. Sincechrome.ttshands speech straight to the OS TTS engine with no access to the raw audio buffer, this works by capturing system audio viagetDisplayMedia({audio: true})while the selection is read aloud — hosted in a newchrome.offscreendocument (reasonDISPLAY_MEDIA), since the MV3 service worker inbackground.jshas no document context of its own to callgetDisplayMedia/MediaRecorderfrom. Auto-downloads with a filename likegraytts-clip-2026-08-13-142201-the-quick-brown-fox.webm(timestamp + first 4 words of the selection) once the reading finishes; falls back to a timestamp-only filename for symbol-only/non-Latin selections. Every failure path (picker cancelled, wrong picker choice, a capture already in progress, achrome.ttserror mid-capture) shows the existing toolbar error badge rather than failing silently. Full design:docs/superpowers/specs/save-audio-clip.md. Chrome's screen-share picker can't be bypassed or remembered, so every clip save requires clicking through "Entire Screen" + the audio checkbox — a hard platform constraint, not a bug. - 2026-08-12: Added read-along highlighting — the word
chrome.ttsis currently speaking is now highlighted live on the page for right-click/hotkey reads (not the popup's Preview, which has no source page). Uses the CSS Custom Highlight API (CSS.highlights+Highlight, declared via a newcontent.cssinmanifest.json'scontent_scripts) rather than wrapping spoken text in new DOM elements, so it never mutates the page's DOM tree — safe on framework-heavy pages (React, etc.) that re-render and would otherwise wipe out or duplicate injected wrapper spans.background.jsrelays eachchrome.tts'word'event'scharIndex/lengthto the source tab;content.jsmaps those offsets onto the actual selectionRangeby walking its text nodes (handles a spoken word split across inline markup, e.g.<b>, correctly). Degrades silently (no highlight, no error) if the API isn't supported. Verified against a live DOM in the Browser pane, including a word split across a<b>boundary and an intentionally-overrunlengthvalue. - 2026-08-12: Added Resume and Stop buttons next to Pause (
chrome.tts.resume()/chrome.tts.stop()). Previously there was no way to resume a paused read or forcibly stop one from the popup. - 2026-08-12: Fixed a bug in the per-language migration added earlier the same session: the
legacy flat settings shape was migrated into a bucket keyed
''(All languages) regardless of which language filter was actually active, so anyone with a language filter set lost their saved voice/rate/pitch/volume on first load after the update. Also: the migrated shape is now persisted back to storage immediately (previously only happened once a control was touched), andbackground.jsnow falls back to reading the legacy flat fields directly if the popup hasn't migrated storage yet — closing a window where right-click/hotkey speech would silently fall back to the system default voice. Caught by a second pair of eyes before it reached Grayson; regression-tested with a standalone Node script (not committed — throwaway). - 2026-08-12: Voice/rate/pitch/volume are now remembered per language (keyed by the Language filter), instead of one global set. Switching the filter now recalls what you last used for that language instead of carrying over whatever was picked for the previous one. Existing single-bucket settings are migrated automatically into the "All languages" bucket on first load, so no one loses their current voice pick.
- 2026-08-12: chrome.tts failures are no longer silent. A
chrome.tts.speak()error now shows a red badge on the toolbar icon (with the error in the tooltip, auto-clearing after ~8s or on the next successful speak) and, for the popup's Preview button specifically, an inline error message under the button. - 2026-08-12: Visual pass on the popup — grouped the controls into labeled sections (Voice,
Sliders, Actions), added a status badge, styled the sliders/buttons/dropdowns, and gave the
Preview button an icon. No functional changes; all element IDs and
popup.jslogic are unchanged. - 2026-08-12: Added a language filter dropdown above the voice list in the popup, since
chrome.tts.getVoices()returns every installed system voice unfiltered (long list). The filter persists across popup opens like the other TTS settings. - 2026-08-12: Replaced ResponsiveVoice with the native
chrome.ttsAPI. ResponsiveVoice played audio by fetching fromresponsivevoice.orginside the content script, which inherits the host page's Content Security Policy — CSP-strict sites (GitHub, etc.) silently blocked the audio load with no visible error.chrome.ttsspeaks natively via the OS/browser TTS engine from the background service worker, so it's never subject to a page's CSP. Also fixed background.js cachingttsSettingsin a variable that reset to{}every time the MV3 service worker respawned (~30s idle), silently losing the selected voice/rate/pitch/volume. Removed responsivevoice.js and rv-config.js (no longer used). - 2026-08-11: Fixed popup.html never loading responsivevoice.js (popup threw on open), an
undeclared
previewingvariable in popup.js, and background.js calling responsiveVoice from the MV3 service worker (no window/document there) instead of relaying to the content script. Added the version_name build stamp described above. - 2023-08-02: Initial release