All notable changes to this project are documented here. Format follows Keep a Changelog; this project follows Semantic Versioning.
segmentation.reconstruct extraction option, and the matching reconstruct preference in SpeechPreferences, SpeechDefaults and SpeechPreferencesEditor (a BooleanPreference): with sentence segmentation, reconstructs a sentence split across blocks. Default false.segmentation: { mode: "sentence" } no longer reconstructs a sentence split across blocks, e.g. two <p>, unless segmentation.reconstruct is true (the reconstruct preference in ReadiumSpeechNavigator, default false), meant for fixed-layout text fragments. A sentence still joins across inline elements of its own block, and across unroled <div>s.<sub alias="…"> (from <abbr title="…">/<acronym title="…"> in @readium/guided-navigation) is read as its abbreviation followed by its alias in parentheses, e.g. “The WHO (World Health Organization) said.”; with interruptions: { abbreviation: "inline" }, the alias is spoken in its place; with "never", only the abbreviation is read. The alias is located on its abbreviation.biblioref and glossref are read like a note reference: the bibliography entry or definition @readium/guided-navigation embeds for a target in the same document follows their interruptions setting. Their own text is always read in place. With "after", a definition is read with its term, e.g. “Fresnel lens: A compact lens for focusing light.”; with them in contextualize, it’s preceded by “Glossary definition.”, and a bibliography entry by “Bibliographic reference.”Interruption type: "never", "inline" or "after".interruptions replaces inlineContextualization, in ExtractUtterancesOptions, SpeechPreferences and SpeechPreferencesEditor, where it’s a Preference of that record instead of a StringArrayPreference: { noteref: "inline", pagebreak: "never" }, each role "never", "inline" or "after" (the default), and interruptionRoles replaces inlineContextualizationRoles. It applies under every verbosity: reading notes or page breaks never no longer takes verbosity: "custom" and skip. skip ignores noteref, pagebreak and biblioref, which leave skippableRoles.verbosity presets no longer skip footnotes, note references, page breaks and bibliography references: skippedAtVerbosity no longer has footnote, noteref, pagebreak or biblioref.@readium/guided-navigation is ^1.2.2.aria-label (e.g. <p><a aria-label="Note 1">[1]</a></p>) is located on that element, or its nearest located ancestor, instead of searching the page for its label’s text.ReadiumSpeechNavigator’s word boundaries after an element’s aria-label in the same utterance (e.g. See <a aria-label="chapter one">1</a> and more words.) locate their words again, instead of none past the label.cancel() replaced ended the restarted one, which Safari still spoke. WebSpeechEngine now waits until 250 ms after it last interrupted speech before speaking there, so speaking another utterance while one is speaking, e.g. going to the next one, starts up to 250 ms later.ReadiumSpeechNavigator’s loadContent() and loadGndContent() take the content’s language as a whole, as { language } (ReadiumSpeechContentOptions). Utterances without a language of their own get it, and languageOverrides applies to it on content loaded via loadGndContent().contentLanguage extraction option: the language of content without lang/xml:lang, and of all content with language: "none". Sentences are segmented in it, and languageOverrides applies to it. loadGndContent()’s { language } is passed as this option.language: "none", utterances keep the content’s language when loadGndContent() is given one, instead of having none, so a voice record (e.g. { en: "…" }) still applies instead of the engine’s own voice.segmentation: "sentence", a sentence of content without lang/xml:lang is in the content’s language when loadGndContent() is given one, instead of English, and with language: "none", sentences are segmented in it, with its abbreviations.segmentation: "sentence", a malformed language tag (e.g. en_US) no longer makes extraction throw: it is segmented as its hyphenated form, else in English.segmentation: "sentence", a closing guillemet spaced after the sentence’s last mark, as in French (« Il n’y a rien ! »), stays with its sentence instead of being an utterance of its own or starting the next one."end" for the restarted utterance without speaking it: Safari ends an utterance spoken right after cancel() at once, so WebSpeechEngine speaks it on the next task there, and once more if Safari still ends it without starting it.WebSpeechEngine no longer takes Chrome, Edge or Opera for Safari, whose name their user agents contain: with a Google voice, they get the workaround for long utterances being cut off, and none of Safari’s. It only runs for voices that fire no pause events, as it pauses and resumes speech every 5 seconds."stop" for the one it replaces, which left ReadiumSpeechNavigator idle until the next one started: Safari reports the cancelled utterance’s error within cancel().ReadiumSpeechNavigator’s word boundaries in substituted text (e.g. 100deg spoken as 100°) locate the source text again when a voice or the content’s language applies to the utterance.ReadiumSpeechUtterance.voice: the voice an engine speaks an utterance with, when it has it — matched by voiceURI (then name) on WebSpeechEngine, by identifier or name on SpeechServerEngine. Without one, engines use their own choice as before.ReadiumSpeechNavigator’s "start" event reports the voice an utterance is spoken with, as detail.voice.voice preference: the voice of every utterance without its own, a single voice id or one per language. ReadiumSpeechNavigator applies it to content loaded via loadContent() and loadGndContent(), and reloads the queue when it changes.languageOverrides extraction option and preference: the language content declared in a given language is spoken in, e.g. { en: "fr" } for French content wrongly declared as English. Keys match by primary subtag, an exact tag winning over it.WebSpeechVoiceManager.getBrowserLanguages(): the languages of every voice the browser has, with voice counts, without loading the voice data of those initialize() wasn’t called with.valueForLanguage(), matching a language-keyed record the way the voice and languageOverrides preferences are, extractLangRegionFromBCP47() and getLanguageDisplayName() are exported, as is getVoiceId(), the id voice takes for a voice.ReadiumSpeechNavigator’s setVoice()/getCurrentVoice() are removed: set voices with the voice preference, and read them from settings.voice, which falls back to the engine’s voice for its language. The voice an utterance is actually spoken with is in the "start" event’s detail.voice. Engines keep theirs, as their own default voice.setSpeakInContentLanguage()/getSpeakInContentLanguage() are removed from engines and ReadiumSpeechNavigator. An utterance is always spoken in its language: with the selected voice when it speaks that language, whatever the region, else with that language’s best voice. Extraction’s language: "none" keeps one voice throughout.inlineContextualization takes a list of roles instead of a boolean, so noteref and pagebreak can be chosen separately. inlineContextualizationRoles lists the accepted roles. true becomes ["noteref", "pagebreak"]; a boolean is now dropped as invalid.5, iv, 五, ה׳, εʹ…) is spoken as “Page 5”, with or without contextualize: ["pagebreak"]. The wording is the catalog’s pagebreak.number entry.contextualizationLocale isn’t shipped), so a voice can be chosen for them like for content. One merged with content in the same primary language (e.g. en with en-US) takes the content’s. In another language, it’s wrapped in a <lang> tag in SSML, and its own utterance in plain text, an inline footnote or pagebreak splitting the sentence holding it around it.skip ignores a role that a more specific one on the same node drops: skipping aside no longer skips <aside epub:type="footnote">, so notes can be kept while sidebars are skipped. Presets are unaffected.@readium/guided-navigation is now ^1.1.3, which gives descriptions and the text of page breaks, note references and links their language.SpeechServerEngine speaks each utterance in its language, like WebSpeechEngine: with the selected voice when it speaks that language, else the best server voice for it, firing "languagefallback" when there is none. Its new initialize() picks a default voice by the browser’s languages, used until a voice is set, instead of sending requests without one.getCurrentVoice() returns the engine’s default voice until one is set, on WebSpeechEngine and SpeechServerEngine, instead of null.WebSpeechEngine has the voices of languages WebSpeechVoiceManager.initialize() is later called with, in getAvailableVoices() and for an utterance’s voice, instead of only those it was initialized with.WebSpeechVoiceManager.convertToSpeechSynthesisVoice() matches a voice’s voiceURI first, then its originalName, then its normalized name: a voice sharing its normalized name with another quality or language (e.g. Apple’s Eddy) no longer resolves to whichever came first.segmentation: "sentence", a paragraph holding an inline footnote or pagebreak is split into sentences, the footnote or pagebreak staying inside its own. It was spoken as one utterance once announced, and split inside the footnote otherwise.segmentation: "sentence", a footnote or pagebreak not in inlineContextualization is spoken right after the sentence holding it, instead of after the whole paragraph.languageOverrides. An announcement holding one in another language than its catalog’s wraps it in a <lang> tag in SSML, and is split around it in plain text, e.g. “Image with a label:” and “Un chat”; in the same language, it stays one utterance in the description’s language.autoPause, or pause() during pauseDuration) resumes at the next utterance, instead of replaying the one that just ended.segmentation: "sentence" and language: "none", a sentence joined across several pieces no longer gets language: "en".fr/FR) is spoken as one language, instead of being kept in separate utterances.skip, contextualize, inlineContextualization, languageOverrides, voice): changing one in place in a preferencesEditor, or in the object passed in, no longer changes the navigator’s preferences without submitPreferences().WebSpeechVoiceManager reports controls.boundary: false for every voice on Android, which never emits boundary events. Voices Chrome names after their language (e.g. “English United States”) aren’t in the JSON, and previously defaulted to supporting boundaries.WebSpeechEngine also emits pause and resume for Google (Natural) and Chrome OS voices, which never fire the native onpause/onresume either.filterByLanguages, filterOutNoveltyVoices, filterOutVeryLowQualityVoices, isNoveltyVoice, isVeryLowQualityVoice, filterByBoundarySupport.sortByQuality, sortByPreferredRegion, sortAlphabetically, sortVoicesByRegions, groupVoicesByLanguage, pickBestVoiceByRegion, compareByPreferredRegion, compareAlphabetically, getQualityValue, createJsonOrderMap.segmentation: "sentence" no longer reconstructs a sentence across a roled container boundary. A <p> inside a <header> no longer glues onto the paragraph after it, and nav-list entries (toc, pagelist, index, landmarks, loa/loi/lot/lov) no longer glue onto each other. Unroled and presentation wrappers still allow joining, so fixed-layout fragments and layout-table cells reconstruct as before.segmentation: "sentence" applies abbreviation suppressions to region-tagged languages. en-US text now gets the built-in en list (no more break after “Mr.”), and segmentation.suppressions entries for en and en-US both apply to it. Language keys match case-insensitively.WebSpeechEngine: a restarted utterance is no longer cut off by late events (e.g. interrupted) from the utterance it replaced.WebSpeechEngine emits pause and resume for Google online voices in desktop Chrome, which never fire the native onpause/onresume.WebSpeechEngine.setCurrentUtteranceIndex() calls onComplete(true) when the index is unchanged, like SpeechServerEngine. Previously it never called it.SpeechServerEngine: changing voice or content language while paused on a long, split utterance no longer emits an error that stopped playback, and setting the current voice again no longer interrupts it.play() after an extraction-affecting submitPreferences() made while paused now resumes playback, instead of staying silent.pauseDuration between utterances no longer stalls playback on resume; it continues with the next utterance.submitPreferences() no longer warns that extraction-affecting preferences have no effect when no content has been loaded yet. The warning now only fires for content loaded via loadContent().WebSpeechEngine.setVoice() no longer jumps back to the first utterance when the voice changes; the current position is kept.LocatorOptions.href: the resource a locate points into, taken from href-qualified textrefs (chapter.xhtml#css(...)). It’s kept on every locate, offsets entry and word boundary, so a sentence reconstructed across resources (e.g. fixed-layout pages stitched in reading order) maps each piece back to its own resource.createLocator() uses href from its options when present, instead of the current document’s.@readium/guided-navigation is now ^1.1.0.AutoPauseScope and ExtractionFormat types.verbosityPresets, languageModes, extractionFormats, autoPauseScopes, segmentationModes, rateRangeConfig, pitchRangeConfig, volumeRangeConfig, pauseDurationRangeConfig (RangeConfig), plus extractionPreferenceKeys. All are frozen.@readium/guided-navigation. makeGnd, parseMarkup, decodeTextref, the #css(...)/#domrange(...) encoders and decoders, and the GndRole, GndMediaType, GndGenerationOptions, TextrefOptions and DomRangeJSON types are no longer exported from @readium/speech; import them from @readium/guided-navigation.extractUtterances(), loadGndContent() and contextualization.params take @readium/shared’s GuidedNavigationObject instead of GndObject. GndObject, GndText and GndDocument are removed.build/; they’re imported from dependencies, with @readium/shared, @readium/decorator and @readium/helpers now as caret ranges (^2.7.0, ^1.2.5, ^1.2.0) instead of exact versions, and @readium/guided-navigation as ^1.0.1, so an app depending on them too shares one copy with this package.extractUtterances()’s options argument is now optional; omitting it uses the defaults.IEnumPreference.supportedValues is now readonly T[] and IRangePreference.supportedRange is now readonly [T, T]; they return the same frozen values the library validates against.build/index.cjs). The package is ESM-only; require("@readium/speech") still works on Node versions that support require() of ES modules.css-selector-generator dependency.TServerVoiceControls.boundary — voices now report boundary-event support (controls.boundary in JSON voice data, matching voices.schema.json), alongside the existing pitch/speed/ssml.TVoiceControls — the pitch/boundary subset of TServerVoiceControls applicable to json/browser voices.FallbackEngineProvider’s mid-playback voice matching, and WebSpeechEngine’s per-content-language voice switching (setSpeakInContentLanguage), now also prefer a voice with the same boundary-event support as the one being replaced, falling back to the wider match when none exists.ReadiumSpeechJSONVoice.pitchControl (boolean) replaced by controls.pitch, matching voices.schema.json; all bundled JSON voice data converted accordingly.ReadiumSpeechVoice.pitchControl — use controls.pitch instead.@readium/decorator@1.2.3.ExtractUtterancesOptions.segmentation — split utterances at real sentence boundaries instead of one per structural unit, reconstructing a sentence across sibling nodes when it genuinely spans them. mode: "sentence" (default "structure"), plus suppressions (per-language abbreviations to not treat as sentence endings) and segmenter (swap in a custom sentence segmenter for the built-in Intl.Segmenter-based one). See UtteranceExtraction.md.ReadiumSpeechUtterance.offsets/UtteranceOffset — the ranges of plain/ssml backed by real source text, each with its own locate; an utterance with none is what synthetic used to flag.ReadiumSpeechNavigator’s "boundary" event, previously only "word", now also fires "sentence"/"structure" (matching the active segmentation mode) whenever the current utterance changes, with detail.locate resolved to a LocatorOptions[]. resolveUtteranceLocate() (sentence/structure) and resolveBoundaryLocate() (word) are the two helpers behind this — the same ones to call yourself when driving playback without ReadiumSpeechNavigator. See Playback.md and Highlighting.md.ReadiumSpeechNavigatorConfiguration.segmentationOverrides and ISpeechPreferences.segmentation/ISpeechDefaults.segmentation — the live segmentation.mode preference, plus construction-time suppressions/segmenter.ExtractUtterancesOptions.substitutions — rewrite ASCII imitations of Unicode symbols (e.g. "1/2", "(c)") so the engine speaks the symbol, merged on top of a built-in table.makeGnd()/parseMarkup()’s textrefs.roles accepts "leaf-text", matching a roleless block that owns its own text directly (e.g. a <div> standing in for <p>). See GuidedNavigation.md.WebSpeechEngine no longer resolves a stray/delayed boundary event from an already-cancelled utterance against whatever utterance is current by the time it arrives.WebSpeechEngine no longer emits a duplicate start/pause/resume event for the same playback action — previously fired once eagerly and again from the native SpeechSynthesisUtterance event, which also doubled ReadiumSpeechNavigator’s synthesized boundary event on start.WebSpeechEngine/SpeechServerEngine now has angle brackets neutralized so plain text containing </> can’t be misread as markup.makeGnd()/parseMarkup()) no longer use attribute selectors, which could latch onto a JS-mutated attribute (e.g. an inline style set by a layout script) and silently stop matching once it changed.ReadiumSpeechUtterance.synthetic — an empty/absent offsets is now what marks an utterance as synthesized rather than sourced.endnotes section no longer skips its own location — previously it was suppressed there unconditionally (since a noteref already speaks it inline), leaving the section’s list silent.noteref no longer merges its Start of the footnote./End of the footnote. contextualization into the same utterance as its content — the note’s real content is now its own separate utterance, consistent with every other block role, instead of being incorrectly flagged synthetic. With inlineContextualization: true, the note is still spoken inline as one merged utterance.biblioref and glossary added to skippableRoles and to skippedAtVerbosity’s none/few/some presets — both are now skippable and, under a preset, extracted only at most.ExtractUtterancesOptions.contextualizationLocale — pick which shipped catalog locale to use.ExtractUtterancesOptions.contextualization — for a custom or overridden catalog: override wording, shape, and params per call.ReadiumSpeechNavigatorConfiguration.contextualizationOverrides — the same overrides, set once for a navigator.ReadiumSpeechUtterance.locate — options for createLocator() to build a locator for the utterance’s source.ReadiumSpeechUtterance.synthetic — true when the utterance’s text isn’t copied from the source, so word-level highlighting can be skipped (element-level highlighting via locate still works).makeGnd()/parseMarkup() can generate textrefs from a live document. See GuidedNavigation.md.cell/rowheader/row contextualizations carry positional context (column header, row number).extractUtterances(), extractUtterancesWithSources(), loadGndContent(), and submitPreferences() are now async.LocatorOptions restructured around text/cssSelector/domRange/fragment.skippableAtVerbosity renamed to skippedAtVerbosity.setRate()/setPitch()/setVolume() now restart the current utterance so changes apply immediately, not on the next one.<th> resolves to columnheader/rowheader instead of always cell.setRate()/setPitch()/setVolume() and playback-rate clamping no longer propagate NaN/±Infinity.defaultAnnouncements, Announcement, AnnouncementKey, AnnouncementPair, Announcements, RoleAnnouncement, isAnnouncementPair — superseded by the JSON contextualization catalog (defaultContextualizations, Contextualizations, ContextualizationEntry).ExtractUtterancesOptions.announcements — superseded by contextualization.contextualizations.FallbackSpeechEngine no longer picks a fallback voice that requires network access unless navigator.onLine is confirmed true, defaulting to offline-only when connectivity is unknown — previously it could pick an online voice while swapping away from a recoverable failure, risking the same failure again.FallbackEngineProvider / FallbackSpeechEngine, wrapping a primary and a fallback ReadiumSpeechEngineProvider as one. onFailure (default "fallback") controls the behavior: "fallback" swaps to the fallback on a recoverable playback failure (mid-playback included) and stays there; "fallbackAndRecover" additionally polls the primary and swaps back once it’s reachable again; "error" never swaps, surfacing failures as-is. No registry involved. See FallbackEngine.md."enginefallback" / "enginerecovered" events on ReadiumSpeechPlaybackEvent, emitted by FallbackSpeechEngine when it swaps engines in either direction.ReadiumSpeechPlaybackEngine.loadUtterances() takes an optional startIndex hint, so an engine that pre-buffers ahead of playback (e.g. SpeechServerEngine) warms the utterances playback will actually resume at instead of always index 0.ReadiumSpeechEngineProvider.getVoices() takes an optional forceRefresh parameter to bypass a provider’s own voice cache — used by FallbackSpeechEngine’s health check so a stale cache can’t report the primary reachable when it isn’t.SpeechServerEngineOptions.timeoutMs, a grace period tolerated after the playback buffer is projected to run dry before a still-pending /synthesize chunk is declared stalled (SpeechServerStallError, 408) and aborted. Default undefined: a stalled request waits forever, same as before this option existed. See Stall detection.SpeechServerNetworkError, thrown when a request to the server never reaches the network at all — distinguishes an actual connectivity failure from an unrelated TypeError thrown later while reading a response that did arrive.ErrorEventDetail, the { message, recoverable, ... } shape an engine’s "error" event detail is expected to follow.SpeechServerAudioDecodeError, thrown when a /synthesize response arrives and parses fine but its audio payload fails to decode.recoverable on every SpeechServerEngine error event’s detail: true when the server never responded at all (network failure or a stall), false when it responded but rejected the request or the audio payload failed to decode. Used by FallbackSpeechEngine to decide whether swapping engines could help.SpeechServerEngine’s "error" event detail always has at least { message, recoverable } now; { status, type, title, instance } is only present for a Problem Details response from the server.SpeechServerEngine catches exceptions thrown by its own event listeners instead of letting one throw out of emitEvent() and abort the others.WebSpeechEngine.loadUtterances() called back-to-back while already "ready" now emits "ready" each time — it previously went through the same diff-based state setter as everything else, which swallowed a same-state transition.SpeechServerEngine/SpeechServerEngineProvider no longer double-apply speed on voices whose provider already applies it server-side (e.g. ElevenLabs). speech-server stopped sending controls per voice on GET /voices, moving it to GET /service’s providers[] only — voice mapping now merges each voice’s controls from there instead of a field the server no longer sends.autoPause preference ("none" | "utterance" | "block", default "none") — fully pauses playback between utterances or blocks (fires pause, navigator.getState() becomes "paused") instead of continuing on its own; playback resumes only when play() is called. See Preferences.md.SpeechDefaults.language default is now "block-level", was "always".ReadiumSpeechUtterance drops startsNewBlock. Block-boundary info is now internal to extraction — autoPause: "block" only has effect on content loaded via loadGndContent(); content loaded via loadContent() never triggers it.pauseDuration always delays the next automatic continuation now that pauseScope is gone — it’s no longer scoped to a subset of transitions.SpeechPreferences.pauseScope / SpeechDefaults.pauseScope, added in 0.6.0. It was meant to implement automatic pausing between utterances/paragraphs but only ever delayed an automatic continuation rather than actually pausing — replaced by autoPause.ReadiumSpeechNavigator (submitPreferences(), settings, preferencesEditor), covering verbosity presets and prosody (pauseDuration, pauseScope, rate, pitch, volume). See Preferences.md.ReadiumSpeechNavigator.loadGndContent(), retaining a Guided Navigation tree so preferences can re-run extraction on it.ReadiumSpeechUtterance.startsNewBlock, marking utterances that begin a new block-level element — used by pauseScope: "block".ExtractUtterancesOptions.contextualize is now GndRole[] (which roles announce), replacing the previous on/off boolean; default is now nothing contextualized.ExtractUtterancesOptions.interruptSentence renamed to inlineContextualization.ReadiumSpeechNavigatorContract drops setRate/getRate/setPitch/getPitch/setVolume/getVolume — use the Preferences API instead.utterances.json now pairs one result with every option-set that produces it (options is an array), instead of repeating identical results per option-set — see fixtures/README.md.SpeechServerEngine / SpeechServerEngineProvider, a second ReadiumSpeechPlaybackEngine implementation backed by a Readium Speech Server HTTP service — with utterance prefetching, chunk-streaming for long/over-limit utterances, gapless Web Audio API scheduling, format/bitrate selection, and RFC 9457 error handling. See SpeechServerEngine.md.ReadiumSpeechProviderRegistry, for registering multiple ReadiumSpeechEngineProviders (e.g. WebSpeech and speech-server) side by side and querying voices/creating engines across all of them. See ProviderRegistry.md.ReadiumSpeechVoice.identifier and ReadiumSpeechVoice.controls (which playback controls a server-sourced voice actually honors), and "server" added to TSource.ReadiumSpeechNavigator (renamed from WebSpeechReadAloudNavigator) is now engine-agnostic: its constructor requires an explicit ReadiumSpeechPlaybackEngine (e.g. new ReadiumSpeechNavigator(new WebSpeechEngine())), instead of defaulting to WebSpeech.WebSpeechEngineProvider.getVoices() no longer requires an engine to have been created first.setSpeakInContentLanguage(enabled) / getSpeakInContentLanguage() on ReadiumSpeechNavigator and ReadiumSpeechPlaybackEngine: when enabled, each utterance is spoken in the best available voice for its own language, falling back to the selected/default voice when none matches (never racing ahead of voice loading and picking the wrong-language voice)."languagefallback" playback event, fired when no voice matches an utterance’s content language (detail: { language, reason: "no-matching-voice" }).WebSpeechVoiceManager.initialize({ languages }) can now be called again on an already-initialized singleton to broaden it to additional languages, instead of being silently ignored.makeGnd, extracting a Guided Navigation (GND) JSON tree from HTML/XHTML.extractUtterances, walking a GND tree into a flat, ordered list of ReadiumSpeechUtterances.@readium/decorator for highlighting, via a new wrapper API that simplifies common usage (including Locator handling).ReadiumSpeechVoice is now fully typed, dropping the any fallback (#48).textContent coercion in demos; README quickstart example.Initial release.