@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 +62 -13
- package/build/WebSpeech/webSpeechEngine.d.ts +1 -1
- package/build/decorator/createLocator.d.ts +9 -0
- package/build/decorator/index.d.ts +2 -0
- package/build/decorator/setupDecorations.d.ts +15 -0
- package/build/gnd/a11y.d.ts +18 -0
- package/build/gnd/converter.d.ts +52 -0
- package/build/gnd/dom.d.ts +5 -0
- package/build/gnd/index.d.ts +4 -0
- package/build/gnd/makeGnd.d.ts +8 -0
- package/build/gnd/object.d.ts +24 -0
- package/build/gnd/roles.d.ts +8 -0
- package/build/gnd/text.d.ts +18 -0
- package/build/gnd/types.d.ts +21 -0
- package/build/index.cjs +43 -20
- package/build/index.d.ts +5 -0
- package/build/index.js +3659 -1195
- package/build/utils/text.d.ts +4 -0
- package/build/utterance.d.ts +2 -2
- package/build/utterances/announcements.d.ts +2 -0
- package/build/utterances/extractUtterances.d.ts +12 -0
- package/build/utterances/index.d.ts +4 -0
- package/build/utterances/language.d.ts +1 -0
- package/build/utterances/roles.d.ts +2 -0
- package/build/utterances/text.d.ts +54 -0
- package/build/utterances/types.d.ts +18 -0
- package/build/voices/languages.d.ts +3 -3
- package/build/voices/types.d.ts +5 -9
- package/package.json +14 -6
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
2. [In-context demo](https://readium.org/speech/demo/article)
|
|
37
|
+
## Demos
|
|
39
38
|
|
|
40
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
- [
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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,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,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
|
+
}
|