feat(announcer): pluggable speech engine with LG/Samsung device detection - #48
Closed
chiefcll wants to merge 1 commit into
Closed
feat(announcer): pluggable speech engine with LG/Samsung device detection#48chiefcll wants to merge 1 commit into
chiefcll wants to merge 1 commit into
Conversation
The Announcer was hard-wired to window.speechSynthesis, so TV platforms
with their own TTS had no way in. The docs pointed at
`Announcer._textToSpeech` as the extension point, which does not exist.
Add `Announcer.setSpeechEngine({ speak, cancel })`, a per-phrase driver.
The Announcer keeps owning the series - flattening, PAUSE- delays, nested
arrays/promises/functions, append, cancel and the network retry - so a
platform integration is a few lines rather than a reimplementation.
Add `Announcer.detectSpeechEngine()` to pick a device's built-in output:
- LG (webos): installs an engine driving luna://com.webos.service.tts.
Each phrase subscribes for feedback and resolves on msgStatus 'done',
so PAUSE- timing mid-series stays accurate. Detection is a capability
test on webOS.service.request rather than a user-agent match, since
Luna is unreachable without webOSTV.js. A request-level failure
(usually a missing com.webos.service.tts permission, which fails for
every phrase) warns once and resolves instead of rejecting, so a
config typo does not become an unhandled rejection per phrase.
- Samsung (tizen): Samsung exposes no API for an app to speak a string -
Voice Guide is the TTS and it only reads the DOM. So detection turns
on the existing `aria` mode instead, and warns when it can tell Voice
Guide is switched off, since nothing is audible in that state.
Also guard `phrase instanceof SpeechSynthesisUtterance`, which throws a
ReferenceError on devices that ship a platform TTS and no Web Speech API
- exactly the devices this targets. window.speechSynthesis is now only
touched by the default engine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Makes the
Announcer's text-to-speech backend replaceable, and adds detection that switches to a TV's own speech output.Announcer.setSpeechEngine({ speak, cancel })The Announcer was hard-wired to
window.speechSynthesis, so platforms with their own TTS had no way in. (The docs pointed atAnnouncer._textToSpeechas the extension point — that property does not exist anywhere in the codebase, so the documented hook was dead.)An engine is a per-phrase driver, not a replacement for the announcer:
The Announcer keeps owning the series — string flattening,
PAUSE-#delays, nested arrays/promises/functions,append,cancel, the 3-attempt network retry — so an integration is a few lines instead of a reimplementation ofspeakSeries. Returning a promise lets the series pace against real speech; returningvoidadvances immediately. Rejections are classified by theirerrorproperty, same codes as the Web Speech API (networkretries,canceled/interruptedend quietly, anything else propagates).Announcer.detectSpeechEngine()Returns
'webos' | 'tizen' | 'default'and configures the matching output. Reads globals only — it never speaks.The two platforms are not symmetric, and that shapes the API:
webos) — installs an engine drivingluna://com.webos.service.tts. Each phrase usesfeedback: true+subscribe: trueand resolves onmsgStatus: 'done', so the promise settles when the TV actually finishes andPAUSE-#mid-series stays accurate.stopped/canceledmap to the benigncanceledcode.tizen) — Samsung provides no API for an app to speak a string. Voice Guide is the TTS and it only reads the DOM; Samsung's documented guidance is ARIA markup. So there is no engine to install — detection turns on the existingariamode, which already writes to anaria-live="assertive"region.default— restores speechSynthesis, leavesariaas configured.Reviewer notes
Three judgment calls worth a look:
webOS.service.request, not a user-agent match — Luna is unreachable unless the app loaded webOSTV.js. On webOS without it we warn and fall back rather than installing an engine that can't work.com.webos.service.ttspermission inappinfo.json, which fails for every phrase; rejecting would turn a one-line config typo into an unhandled rejection per phrase with no indication of the cause. Per-messagemsgStatus: 'error'still rejects.webapis.tvinforeports it off;isTizenVoiceGuideEnabled()is exported for apps that want to react. Documented limits of that path:Announcer.voiceis ignored, and completion isn't observable so a series resolves once labels are written.Also fixed along the way:
phrase instanceof SpeechSynthesisUtterancethrew aReferenceErroron devices that ship a platform TTS and no Web Speech API — exactly the devices this feature targets — so it's now guarded.window.speechSynthesisis only touched by the default engine. Dropped theutterancesarray inspeakSeriesthat was pushed to but never read.Testing
npm test— 169 pass (18 new acrosstests/announcer-engine.spec.tsandtests/announcer-platform.spec.ts: engine routing, lang/voice pass-through, PAUSE ordering, async sequencing, cancel, reset; Luna call shape, appID, done/stopped/failure paths, cancel→stop, and each detection branch driven through awebOS.service.requeststub).npm run tscclean,npm run lint0 errors.API surface is additive — no existing behavior changes when no engine is set and
detectSpeechEngine()isn't called.Docs:
docs/primitives/a11y.mdgains "Custom Speech Engine" and "Device Detection (LG & Samsung)" sections, plus the corrected_textToSpeechreference.🤖 Generated with Claude Code