@readium/speech 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,22 +30,34 @@ In the second phase, we focused on implementing a WebSpeech API-based solution w
30
30
 
31
31
  Key features include advanced voice selection, cross-browser playback control, flexible content loading, and comprehensive event handling for UI feedback. The architecture is designed to be extensible for different TTS backends while maintaining TypeScript-first development practices.
32
32
 
33
- ## Demos
33
+ In the third phase, we added highlighting: content currently being spoken (e.g. the current word or sentence) can be highlighted as playback progresses. See the [Highlighting guide](docs/Highlighting.md).
34
34
 
35
- Two live demos are available:
35
+ We are now focused on the fourth phase: extracting [Guided Navigation objects](https://readium.org/guided-navigation) from a document (or a fragment of a document), and generating utterances from these objects.
36
36
 
37
- 1. [Voice selection with playback demo](https://readium.org/speech/demo)
38
- 2. [In-context demo](https://readium.org/speech/demo/article)
37
+ ## Demos
39
38
 
40
- The first demo showcases the following features:
39
+ ### [Voice selection with playback demo](https://readium.org/speech/demo)
41
40
 
42
41
  - fetching a list of all available languages, translating them to the user's locale and sorting them based on these translations
43
42
  - returning a list of voices for a given language, grouped by region and sorted based on quality
44
43
  - filtering languages and voices based on gender and offline availability
45
44
  - using embedded test utterances to demo voices
46
45
  - using the current Navigator for playback control
46
+ - highlighting: as playback progresses, the current word/sentence is highlighted
47
+
48
+ ### [In-context demo](https://readium.org/speech/demo/article)
49
+
50
+ In-context reading with seamless voice selection (grouped by region and sorted based on quality), and playback control, providing an optional read-along experience that integrates naturally with the content. Also showcases highlighting, as above.
51
+
52
+ ### [Extraction playground](https://readium.org/speech/demo/playground)
47
53
 
48
- The second demo focuses on in-context reading with seamless voice selection (grouped by region and sorted based on quality), and playback control, providing an optional read-along experience that integrates naturally with the content.
54
+ Pick a sample piece of markup and watch it move through the whole pipeline:
55
+
56
+ - the [Guided Navigation](docs/GuidedNavigation.md) document it produces
57
+ - the [utterances](docs/UtteranceExtraction.md) extracted from that, with every extraction option (format, language handling, skipped roles, sentence interruption, contextualization) adjustable live
58
+ - playback of the resulting utterances through the WebSpeech navigator, with word-boundary highlighting
59
+
60
+ The sample markup is drawn from this project's own conformance test suite ([`fixtures/`](fixtures/README.md)), so each pane also shows a pass/fail badge against that suite's expected output — a side effect of reusing real test content, not the point of the demo.
49
61
 
50
62
  ## Installation
51
63
 
@@ -64,7 +76,12 @@ yarn add @readium/speech
64
76
  ## Quick Start
65
77
 
66
78
  ```typescript
67
- import { WebSpeechVoiceManager, WebSpeechReadAloudNavigator } from "@readium/speech";
79
+ import {
80
+ WebSpeechVoiceManager,
81
+ WebSpeechReadAloudNavigator,
82
+ setupDecorations,
83
+ DecorationStyleType,
84
+ } from "@readium/speech";
68
85
 
69
86
  // Initialize voice manager
70
87
  const voiceManager = await WebSpeechVoiceManager.initialize({
@@ -78,24 +95,48 @@ const voice = await voiceManager.getDefaultVoice("en-US");
78
95
  const navigator = new WebSpeechReadAloudNavigator();
79
96
  await navigator.setVoice(voice);
80
97
 
98
+ const content = document.getElementById("content");
99
+ if (!content) throw new Error("Missing #content element");
100
+
101
+ // Set up highlighting for the current window
102
+ const decorations = setupDecorations();
103
+
81
104
  // Handle playback events
82
105
  navigator.on("play", () => console.log("Playback started"));
83
106
  navigator.on("pause", () => console.log("Playback paused"));
84
107
  navigator.on("end", () => console.log("Playback completed"));
85
108
 
109
+ // Highlight each word as it's spoken
110
+ navigator.on("boundary", (event) => {
111
+ const { charIndex, charLength } = event.detail;
112
+ const utterance = content.textContent ?? "";
113
+ const word = utterance.substring(charIndex, charIndex + charLength);
114
+
115
+ decorations.decorate([{
116
+ id: "tts-word",
117
+ style: { type: DecorationStyleType.Highlight, tint: "#ffeb3b" },
118
+ highlight: word,
119
+ }], "tts");
120
+ });
121
+
86
122
  // Load and play content
87
- const content = document.getElementById("content");
88
123
  navigator.loadContent(content);
89
124
  navigator.play();
90
125
  ```
91
126
 
127
+ See the [Highlighting guide](docs/Highlighting.md) for the other ways to build and apply decorations.
128
+
92
129
  ## Docs
93
130
 
94
131
  Documentation provides guides for:
95
132
 
96
133
  - [SpeechSynthesis in browsers and OSes](docs/WebSpeech.md)
97
134
  - [Voices and Filtering](docs/VoicesAndFiltering.md)
98
- - [API Reference](docs/API.md)
135
+ - [Voice Management](docs/VoiceManagement.md)
136
+ - [Playback API](docs/Playback.md)
137
+ - [Highlighting](docs/Highlighting.md)
138
+ - [Guided Navigation](docs/GuidedNavigation.md) — extracting [Guided Navigation objects](https://readium.org/guided-navigation) from HTML/XHTML content
139
+ - [Utterance Extraction](docs/UtteranceExtraction.md) — extracting utterances from Guided Navigation objects
99
140
 
100
141
  ## Development
101
142
 
@@ -117,7 +158,7 @@ This will compile the TypeScript code and generate the following outputs in the
117
158
 
118
159
  ### Running Demos Locally
119
160
 
120
- The project includes two demo applications that can be served locally:
161
+ The project includes demo applications that can be served locally:
121
162
 
122
163
  1. Start the local development server:
123
164
  ```bash
@@ -127,6 +168,7 @@ The project includes two demo applications that can be served locally:
127
168
  2. Open your browser to:
128
169
  - [Voice selection demo](http://localhost:8080/demo)
129
170
  - [In-context reading demo](http://localhost:8080/demo/article)
171
+ - [Extraction playground](http://localhost:8080/demo/playground)
130
172
 
131
173
  ### ChromeOS Debugging
132
174
 
@@ -136,12 +178,19 @@ For ChromeOS development, the project includes a debug mode that mocks the Web S
136
178
 
137
179
  2. The debug page loads mock voices from a json file which contains a snapshot of ChromeOS voices.
138
180
 
139
- ### Running Tests
181
+ ### Testing
182
+
183
+ `npm test` builds the library and runs the full test suite (`ava`) across `test/**/*.test.ts`. Narrower scripts are available for working on one area at a time:
140
184
 
141
- To run the test suite for `WebSpeechVoiceManager`:
142
185
  ```bash
143
- npm test
186
+ npm test # build + full suite
187
+ npm run test:voices # WebSpeechVoiceManager only
188
+ npm run test:gnd # HTML/XHTML -> Guided Navigation conversion
189
+ npm run test:utterances # Guided Navigation -> utterance extraction
144
190
  ```
191
+
192
+ `test:gnd` and `test:utterances` are both driven by [`fixtures/`](fixtures/README.md), a language-agnostic conformance suite of paired input/expected-output files (`input.html`/`input.xhtml`, `gnd.json`, `utterances.json`) covering the [Guided Navigation](docs/GuidedNavigation.md) and [utterance extraction](docs/UtteranceExtraction.md) stages one role/encoding/option at a time. Each fixture is a plain-file test case any platform implementation can consume, not just this TypeScript one — see [fixtures/README.md](fixtures/README.md) for the format, how to add a fixture, and how a fixture "passes".
193
+
145
194
  ## Acknowledgments
146
195
 
147
196
  This project is based on the work done initially by [Hadrien Gardeur](https://github.com/hadriengardeur) in the [web-speech-recommended-voices](https://github.com/HadrienGardeur/web-speech-recommended-voices) repository.
@@ -38,7 +38,7 @@ export declare class WebSpeechEngine implements ReadiumSpeechPlaybackEngine {
38
38
  private validateText;
39
39
  private getCurrentVoiceForUtterance;
40
40
  getCurrentVoice(): ReadiumSpeechVoice | null;
41
- private escapeSSML;
41
+ private toPlainText;
42
42
  loadUtterances(contents: ReadiumSpeechUtterance[]): void;
43
43
  setVoice(voice: ReadiumSpeechVoice | string): Promise<void>;
44
44
  getAvailableVoices(): Promise<ReadiumSpeechVoice[]>;
@@ -0,0 +1,9 @@
1
+ import { Locator } from '@readium/shared';
2
+ export interface LocatorOptions {
3
+ highlight?: string;
4
+ before?: string;
5
+ after?: string;
6
+ selector?: string;
7
+ fragment?: string;
8
+ }
9
+ export declare function createLocator(options: LocatorOptions, wnd?: Window): Locator;
@@ -0,0 +1,2 @@
1
+ export * from './setupDecorations';
2
+ export * from './createLocator';
@@ -0,0 +1,15 @@
1
+ import { DirectCommsChannel, Decorator, DecorationController, DecorationControllerConfig, DecorationStyle } from '@readium/decorator';
2
+ import { LocatorOptions } from './createLocator';
3
+ export interface DecorationInput extends LocatorOptions {
4
+ id: string;
5
+ style: DecorationStyle;
6
+ }
7
+ export declare class ReadiumSpeechDecorationController extends DecorationController {
8
+ private readonly channel;
9
+ private readonly wnd;
10
+ private readonly decorator;
11
+ constructor(channel: DirectCommsChannel, wnd: Window, decorator: Decorator, config?: DecorationControllerConfig);
12
+ decorate(decorations: DecorationInput[], group: string): void;
13
+ destroy(): void;
14
+ }
15
+ export declare function setupDecorations(wnd?: Window, config?: DecorationControllerConfig): ReadiumSpeechDecorationController;
@@ -0,0 +1,18 @@
1
+ import { GndText } from './types.js';
2
+ /** Normalized (whitespace-coalesced and trimmed) text content of a node's subtree. */
3
+ export declare function normalizedNodeText(el: Node): string;
4
+ /**
5
+ * Computes the text that becomes a node's `GndObject.description`, and
6
+ * whether the node is visible in the first place. Follows the AccName
7
+ * precedence order (https://www.w3.org/TR/accname/#terminology, 2.A-2.D)
8
+ * for its accessible-name sources, with a non-AccName `aria-describedby`
9
+ * fallback spliced in between 2.C and 2.D — see that branch below.
10
+ */
11
+ export declare function extractNodeAria(el: Element): [GndText | null, boolean];
12
+ /**
13
+ * Maps an HTML element to the SSML tag its text should be wrapped in.
14
+ * https://www.w3.org/TR/speech-synthesis11/#S3.2.2
15
+ */
16
+ export declare function convertElementToSSMLTag(tagName: string): [string, Record<string, string>?];
17
+ /** Elements whose entire subtree carries no user-facing content. */
18
+ export declare const skippedElements: Set<string>;
@@ -0,0 +1,52 @@
1
+ import { GndObject } from './types.js';
2
+ import { GndMediaType } from './dom.js';
3
+ /** Walks a DOM subtree, building the Guided Navigation object tree. */
4
+ export declare class Converter {
5
+ xmlParsed: boolean;
6
+ ids: Map<string, Element>;
7
+ suppressed: Set<Element>;
8
+ idAlloc: {
9
+ claimed: Set<string>;
10
+ counters: Map<string, number>;
11
+ };
12
+ noterefDepth: number;
13
+ allowNode: Element | null;
14
+ private root;
15
+ private current;
16
+ private segments;
17
+ private textAcc;
18
+ private currentCtx;
19
+ private flowEndsWithSpace;
20
+ private pendingChildren;
21
+ constructor(xmlParsed: boolean);
22
+ private allocateId;
23
+ private claimId;
24
+ prescan(root: Element): void;
25
+ convert(root: Element): void;
26
+ convertChildren(root: Element): void;
27
+ result(): GndObject[];
28
+ private descend;
29
+ private appendChild;
30
+ private walk;
31
+ private head;
32
+ private tail;
33
+ private text;
34
+ private textContext;
35
+ private updateFlowSpace;
36
+ private closeSegment;
37
+ private resetFlow;
38
+ private placeholder;
39
+ private pagebreak;
40
+ private noteref;
41
+ private link;
42
+ private flushText;
43
+ }
44
+ /**
45
+ * Converts an HTML or XHTML fragment or document into Guided Navigation
46
+ * objects, reflecting exactly the input it's given: a real, author-written
47
+ * <body> becomes its own role: ["body"] node like any other element; a
48
+ * <body> synthesized only by text/html parsing around a bodyless fragment
49
+ * is not content and is skipped through; a bodyless XHTML fragment's root
50
+ * element is itself the content.
51
+ */
52
+ export declare function parseMarkup(input: string, mediaType?: GndMediaType): GndObject[];
@@ -0,0 +1,5 @@
1
+ export declare function nodeLanguage(el: Element | null): string;
2
+ export declare function hasElementChild(el: Element): boolean;
3
+ export declare function isAncestorOf(anc: Element, n: Element): boolean;
4
+ export type GndMediaType = "text/html" | "application/xhtml+xml";
5
+ export declare function sniffMediaType(input: string): GndMediaType;
@@ -0,0 +1,4 @@
1
+ export type { GndRole, GndText, GndObject, GndDocument } from './types.js';
2
+ export { makeGnd } from './makeGnd.js';
3
+ export { parseMarkup } from './converter.js';
4
+ export type { GndMediaType } from './dom.js';
@@ -0,0 +1,8 @@
1
+ import { GndDocument } from './types.js';
2
+ import { GndMediaType } from './dom.js';
3
+ export type { GndMediaType };
4
+ /**
5
+ * Builds a Guided Navigation document from an HTML or XHTML fragment or
6
+ * document, following https://github.com/readium/guided-navigation.
7
+ */
8
+ export declare function makeGnd(input: string, mediaType?: GndMediaType): GndDocument;
@@ -0,0 +1,24 @@
1
+ import { GndObject, GndRole } from './types.js';
2
+ import { TextBuilder } from './text.js';
3
+ export interface ObjBuilder {
4
+ id?: string;
5
+ audioref?: string;
6
+ imgref?: string;
7
+ textref?: string;
8
+ videoref?: string;
9
+ text?: TextBuilder;
10
+ role?: GndRole[];
11
+ children?: ObjBuilder[];
12
+ description?: string;
13
+ }
14
+ export declare function isEmptyObj(o: ObjBuilder): boolean;
15
+ /** A node being built up during the tree walk, before its final shape is known. */
16
+ export declare class NavObject {
17
+ el?: Element;
18
+ object: ObjBuilder;
19
+ children: NavObject[];
20
+ noText: boolean;
21
+ finalize(): ObjBuilder;
22
+ }
23
+ export declare function finalizeToGndObject(o: ObjBuilder): GndObject;
24
+ export declare function gndObjectToObjBuilder(n: GndObject): ObjBuilder;
@@ -0,0 +1,8 @@
1
+ import { GndRole } from './types.js';
2
+ /**
3
+ * Determines the Guided Navigation roles of an element, combining the roles
4
+ * derived from the element type itself with the ones from its ARIA `role` and
5
+ * `epub:type` attributes, e.g. `<section epub:type="chapter">` -> `[section, chapter]`.
6
+ * An ARIA role of "presentation"/"none" strips the element of its native semantics.
7
+ */
8
+ export declare function extractNodeRoles(el: Element): GndRole[];
@@ -0,0 +1,18 @@
1
+ import { GndText } from './types.js';
2
+ export interface TextBuilder {
3
+ plain: string;
4
+ ssml: string;
5
+ language: string;
6
+ }
7
+ export declare function textIsEmpty(t: TextBuilder): boolean;
8
+ export declare function finalizeText(t?: TextBuilder): string | GndText | undefined;
9
+ export interface SSMLContext {
10
+ lang: string;
11
+ tag: string;
12
+ attrs?: Record<string, string>;
13
+ }
14
+ export declare function ctxEqual(a: SSMLContext, b: SSMLContext): boolean;
15
+ export declare const ssmlTextEscape: (s: string) => string;
16
+ export declare const ssmlAttrEscape: (s: string) => string;
17
+ export declare function isNoBreakSpace(ch: string): boolean;
18
+ export declare function normalizeWhitespace(text: string, stripLeading: boolean): string;
@@ -0,0 +1,21 @@
1
+ export type GndRole = string;
2
+ export interface GndText {
3
+ language: string;
4
+ plain?: string;
5
+ ssml?: string;
6
+ }
7
+ export interface GndObject {
8
+ role?: GndRole[];
9
+ text?: string | GndText;
10
+ description?: string;
11
+ imgref?: string;
12
+ audioref?: string;
13
+ videoref?: string;
14
+ textref?: string;
15
+ id?: string;
16
+ children?: GndObject[];
17
+ }
18
+ export interface GndDocument {
19
+ links?: unknown[];
20
+ guided: GndObject[];
21
+ }