@readium/speech 0.3.0 → 0.5.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
@@ -24,7 +24,7 @@ The outline of this work has been explored in a [GitHub discussion](https://gith
24
24
  In the second phase, we focused on implementing a WebSpeech API-based solution with an architecture designed for future extensibility:
25
25
 
26
26
  - **Engine Layer**: Core TTS functionality through `ReadiumSpeechPlaybackEngine`
27
- - **Navigator Layer**: Content and playback management via (a temporary) `ReadiumSpeechNavigator`
27
+ - **Navigator Layer**: Content and playback management via `ReadiumSpeechNavigator`
28
28
  - **Current Implementation**: WebSpeech API with cross-browser compatibility
29
29
  - **Future-Proof Design**: Architecture prepared for additional TTS service adapters
30
30
 
@@ -32,7 +32,9 @@ Key features include advanced voice selection, cross-browser playback control, f
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
- 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.
35
+ In the fourth phase, we extracted [Guided Navigation objects](https://readium.org/guided-navigation) from a document (or a fragment of a document), and generated utterances from these objects.
36
+
37
+ We are now on the fifth phase: a second `ReadiumSpeechPlaybackEngine` implementation, backed by [speech-server](https://github.com/readium/speech-server) instead of the browser's Web Speech API.
36
38
 
37
39
  ## Demos
38
40
 
@@ -78,7 +80,8 @@ yarn add @readium/speech
78
80
  ```typescript
79
81
  import {
80
82
  WebSpeechVoiceManager,
81
- WebSpeechReadAloudNavigator,
83
+ WebSpeechEngine,
84
+ ReadiumSpeechNavigator,
82
85
  setupDecorations,
83
86
  DecorationStyleType,
84
87
  } from "@readium/speech";
@@ -92,7 +95,7 @@ const voiceManager = await WebSpeechVoiceManager.initialize({
92
95
  const voice = await voiceManager.getDefaultVoice("en-US");
93
96
 
94
97
  // Create a navigator instance
95
- const navigator = new WebSpeechReadAloudNavigator();
98
+ const navigator = new ReadiumSpeechNavigator(new WebSpeechEngine());
96
99
  await navigator.setVoice(voice);
97
100
 
98
101
  const content = document.getElementById("content");
@@ -137,6 +140,7 @@ Documentation provides guides for:
137
140
  - [Highlighting](docs/Highlighting.md)
138
141
  - [Guided Navigation](docs/GuidedNavigation.md) — extracting [Guided Navigation objects](https://readium.org/guided-navigation) from HTML/XHTML content
139
142
  - [Utterance Extraction](docs/UtteranceExtraction.md) — extracting utterances from Guided Navigation objects
143
+ - [Provider Registry](docs/ProviderRegistry.md) — using more than one `ReadiumSpeechEngineProvider` (e.g. WebSpeech and speech-server) side by side
140
144
 
141
145
  ## Development
142
146
 
@@ -0,0 +1,6 @@
1
+ export interface TextChunk {
2
+ text: string;
3
+ offset: number;
4
+ }
5
+ export declare function chunkPlainText(text: string, maxLength: number): TextChunk[];
6
+ export declare function chunkSsmlText(text: string, maxLength: number): TextChunk[];
@@ -0,0 +1,20 @@
1
+ export interface SpeechServerProblemDetails {
2
+ type: string;
3
+ title: string;
4
+ status: number;
5
+ detail: string;
6
+ instance?: string;
7
+ }
8
+ export declare class SpeechServerError extends Error {
9
+ readonly status: number;
10
+ readonly type?: string;
11
+ readonly title?: string;
12
+ readonly instance?: string;
13
+ constructor(message: string, options: {
14
+ status: number;
15
+ type?: string;
16
+ title?: string;
17
+ instance?: string;
18
+ });
19
+ }
20
+ export declare function toSpeechServerError(response: Response): Promise<SpeechServerError>;
@@ -0,0 +1,7 @@
1
+ export * from './chunkText';
2
+ export * from './selectFormat';
3
+ export * from './speechServerEngine';
4
+ export * from './speechServerEngineProvider';
5
+ export * from './speechServerVoiceMapping';
6
+ export * from './errors';
7
+ export * from './types';
@@ -0,0 +1,18 @@
1
+ import { SpeechServerAudioFormat } from './types';
2
+ export type CanPlayTypeResult = "probably" | "maybe" | "";
3
+ export type CanPlayType = (mime: string) => CanPlayTypeResult;
4
+ export declare function mimeTypeForFormat(format: string): string;
5
+ export interface SpeechServerFormatOptions {
6
+ preferredFormat?: SpeechServerAudioFormat | (string & {});
7
+ strategy?: "quality" | "bandwidth";
8
+ adaptBitrateToNetwork?: boolean;
9
+ }
10
+ export declare function selectFormat(output: {
11
+ formats: string[];
12
+ default: string;
13
+ }, options: Pick<SpeechServerFormatOptions, "preferredFormat" | "strategy">, canPlay: CanPlayType): string;
14
+ export interface NetworkInfo {
15
+ saveData?: boolean;
16
+ effectiveType?: string;
17
+ }
18
+ export declare function selectBitrate(format: string, adaptBitrateToNetwork: boolean, network: NetworkInfo | undefined): number | undefined;
@@ -0,0 +1,91 @@
1
+ import { ReadiumSpeechPlaybackEngine } from '../engine';
2
+ import { ReadiumSpeechPlaybackEvent, ReadiumSpeechPlaybackState } from '../navigator';
3
+ import { ReadiumSpeechUtterance } from '../utterance';
4
+ import { ReadiumSpeechVoice } from '../voices/types';
5
+ import { SpeechServerFormatOptions } from './selectFormat';
6
+ export interface SpeechServerEndpoints {
7
+ voices: string;
8
+ synthesize: string;
9
+ service: string;
10
+ }
11
+ export interface SpeechServerEngineOptions {
12
+ endpoints: SpeechServerEndpoints;
13
+ fetch?: typeof fetch;
14
+ prefetchWindow?: number;
15
+ readyBufferChars?: number;
16
+ overLengthText?: "split" | "error";
17
+ format?: SpeechServerFormatOptions;
18
+ }
19
+ export declare class SpeechServerEngine implements ReadiumSpeechPlaybackEngine {
20
+ private endpoints;
21
+ private fetchImpl;
22
+ private currentVoice;
23
+ private voices;
24
+ private serviceInfo;
25
+ private serviceInfoPromise;
26
+ private currentUtterances;
27
+ private currentUtteranceIndex;
28
+ private playbackState;
29
+ private eventListeners;
30
+ private speakInContentLanguage;
31
+ private speakGeneration;
32
+ private loadGeneration;
33
+ private readonly prefetchWindow;
34
+ private readonly readyBufferChars;
35
+ private readonly overLengthText;
36
+ private readonly formatOptions;
37
+ private readonly canPlayType;
38
+ private prefetchCache;
39
+ private prefetchChainTail;
40
+ private audioContext;
41
+ private masterGain;
42
+ private scheduledChunks;
43
+ private boundaryRafHandle;
44
+ private rate;
45
+ private pitch;
46
+ private volume;
47
+ constructor(options: SpeechServerEngineOptions);
48
+ setAvailableVoices(voices: ReadiumSpeechVoice[]): void;
49
+ loadUtterances(contents: ReadiumSpeechUtterance[]): void;
50
+ private bufferUntilReady;
51
+ private indexCoveringChars;
52
+ setVoice(voice: ReadiumSpeechVoice | string): void;
53
+ getCurrentVoice(): ReadiumSpeechVoice | null;
54
+ getAvailableVoices(): Promise<ReadiumSpeechVoice[]>;
55
+ private getServiceInfo;
56
+ private fetchServiceInfo;
57
+ setSpeakInContentLanguage(enabled: boolean): void;
58
+ getSpeakInContentLanguage(): boolean;
59
+ speak(utteranceIndex?: number): void;
60
+ private synthesizeAndPlay;
61
+ private resolveSynthesisStream;
62
+ private fillPrefetchWindow;
63
+ private queuePrefetch;
64
+ private clearPrefetchCache;
65
+ private synthesizeStream;
66
+ private synthesizeChunk;
67
+ private ensureAudioContext;
68
+ private scheduleChunksStreaming;
69
+ private handleUtteranceEnded;
70
+ private startBoundaryPolling;
71
+ private stopBoundaryPolling;
72
+ private checkBoundaries;
73
+ private stopAudio;
74
+ pause(): void;
75
+ resume(): void;
76
+ stop(): void;
77
+ setRate(rate: number): void;
78
+ getRate(): number;
79
+ setPitch(pitch: number): void;
80
+ getPitch(): number;
81
+ setVolume(volume: number): void;
82
+ getVolume(): number;
83
+ getState(): ReadiumSpeechPlaybackState;
84
+ getCurrentUtteranceIndex(): number;
85
+ setCurrentUtteranceIndex(index: number, onComplete?: (success: boolean) => void): void;
86
+ getUtteranceCount(): number;
87
+ on(event: ReadiumSpeechPlaybackEvent["type"], callback: (event: ReadiumSpeechPlaybackEvent) => void): () => void;
88
+ private emitEvent;
89
+ private setState;
90
+ destroy(): Promise<void>;
91
+ }
@@ -0,0 +1,16 @@
1
+ import { ReadiumSpeechEngineProvider } from '../provider';
2
+ import { ReadiumSpeechPlaybackEngine } from '../engine';
3
+ import { ReadiumSpeechVoice } from '../voices/types';
4
+ import { SpeechServerEngineOptions } from './speechServerEngine';
5
+ export type SpeechServerEngineProviderOptions = SpeechServerEngineOptions;
6
+ export declare class SpeechServerEngineProvider implements ReadiumSpeechEngineProvider {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ private options;
10
+ private fetchImpl;
11
+ private voices;
12
+ constructor(options: SpeechServerEngineProviderOptions);
13
+ getVoices(): Promise<ReadiumSpeechVoice[]>;
14
+ createEngine(voice?: ReadiumSpeechVoice | string): Promise<ReadiumSpeechPlaybackEngine>;
15
+ destroy(): Promise<void>;
16
+ }
@@ -0,0 +1,3 @@
1
+ import { ReadiumSpeechVoice } from '../voices/types';
2
+ import { SpeechServerVoice } from './types';
3
+ export declare function mapServerVoice(voice: SpeechServerVoice): ReadiumSpeechVoice;
@@ -0,0 +1,56 @@
1
+ import { TGender, TQuality, TServerVoiceControls } from '../voices/types';
2
+ export interface SpeechServerVoice {
3
+ name: string;
4
+ originalName: string;
5
+ provider: string;
6
+ identifier: string;
7
+ language: string;
8
+ otherLanguages?: string[];
9
+ gender?: TGender | null;
10
+ quality?: TQuality;
11
+ controls?: TServerVoiceControls;
12
+ }
13
+ export interface SpeechServerTimingMark {
14
+ name: "word" | "sentence";
15
+ charIndex: number;
16
+ charLength: number;
17
+ elapsedTime: number;
18
+ }
19
+ export type SpeechServerAudioFormat = "wav" | "mp3" | "opus" | "aac" | "flac" | "ogg" | "webm" | "m4a";
20
+ export interface SpeechServerSynthesizeRequest {
21
+ id?: string;
22
+ text: string;
23
+ ssml?: boolean;
24
+ language?: string;
25
+ voice?: string;
26
+ prev_utterance?: string;
27
+ next_utterance?: string;
28
+ publication_id?: string;
29
+ boundary?: boolean;
30
+ output?: {
31
+ format?: SpeechServerAudioFormat;
32
+ bitrate?: number | null;
33
+ sample_rate?: number | null;
34
+ speed?: number;
35
+ pitch?: number | null;
36
+ };
37
+ }
38
+ export interface SpeechServerSynthesizeBoundaryResponse {
39
+ audio: string;
40
+ format: SpeechServerAudioFormat;
41
+ boundaries: SpeechServerTimingMark[] | null;
42
+ }
43
+ export interface SpeechServerServiceInfo {
44
+ output: {
45
+ formats: SpeechServerAudioFormat[];
46
+ default: SpeechServerAudioFormat;
47
+ };
48
+ limits: {
49
+ maxTextLength: number;
50
+ maxConcurrentSyntheses: number;
51
+ };
52
+ providers: {
53
+ id: string;
54
+ installedLanguages: string[];
55
+ }[];
56
+ }
@@ -41,13 +41,19 @@ export declare class WebSpeechVoiceManager {
41
41
  private voices;
42
42
  private browserVoices;
43
43
  private isInitialized;
44
+ private scopedLanguages;
45
+ private broadenPromises;
44
46
  private constructor();
45
47
  /**
46
- * Initialize voice manager
48
+ * Initialize voice manager, or broaden an already-initialized singleton to
49
+ * also cover new languages. `languages` scope voice parsing to reduce
50
+ * per-language JSON loading; omitting it on the first call loads everything,
51
+ * but omitting it on a later call to an already-scoped instance is a no-op,
52
+ * not a retroactive broaden-to-everything.
47
53
  * @param options Configuration options for voice loading
48
- * @param options.languages Optional array of preferred language codes to filter voices during initialization
49
- * @param options.maxTimeout Maximum time in milliseconds to wait for voices to load (passed to getBrowserVoices)
50
- * @param options.interval Interval in milliseconds between voice loading checks (passed to getBrowserVoices)
54
+ * @param options.languages Optional array of preferred language codes to filter (or broaden to) voices
55
+ * @param options.maxTimeout Maximum time in milliseconds to wait for voices to load (passed to getBrowserVoices, first call only)
56
+ * @param options.interval Interval in milliseconds between voice loading checks (passed to getBrowserVoices, first call only)
51
57
  * @returns Promise that resolves with the WebSpeechVoiceManager instance
52
58
  */
53
59
  static initialize(options?: {
@@ -60,6 +66,18 @@ export declare class WebSpeechVoiceManager {
60
66
  * @private
61
67
  */
62
68
  private filterBrowserVoicesByLanguages;
69
+ /**
70
+ * Extract base language codes (e.g. "en", "fr") from a list of BCP47 tags
71
+ * @private
72
+ */
73
+ private static toBaseLangSet;
74
+ /**
75
+ * Broaden an already-initialized instance to also cover the given languages,
76
+ * reusing the already-fetched `browserVoices` (no new speechSynthesis fetch).
77
+ * No-op if the instance is already unscoped or already covers these languages.
78
+ * @private
79
+ */
80
+ private broadenLanguages;
63
81
  /**
64
82
  * Extract language and region from BCP47 language tag
65
83
  * @param lang - The BCP47 language tag (e.g., "en-US", "zh-CN")
@@ -1,4 +1,3 @@
1
1
  export * from './WebSpeechVoiceManager';
2
2
  export * from './webSpeechEngine';
3
3
  export * from './webSpeechEngineProvider';
4
- export * from './TmpNavigator';
@@ -13,6 +13,10 @@ export declare class WebSpeechEngine implements ReadiumSpeechPlaybackEngine {
13
13
  private voiceManager;
14
14
  private voices;
15
15
  private defaultVoice;
16
+ private speakInContentLanguage;
17
+ private languageVoiceCache;
18
+ private warmingLanguages;
19
+ private speakGeneration;
16
20
  private resumeInfinityTimer?;
17
21
  private isSpeakingInternal;
18
22
  private isPausedInternal;
@@ -37,7 +41,13 @@ export declare class WebSpeechEngine implements ReadiumSpeechPlaybackEngine {
37
41
  }): Promise<boolean>;
38
42
  private validateText;
39
43
  private getCurrentVoiceForUtterance;
44
+ private voiceMatchesLanguage;
45
+ private voiceForUtteranceSync;
46
+ private voiceForUtterance;
47
+ private warmLanguageVoiceCache;
40
48
  getCurrentVoice(): ReadiumSpeechVoice | null;
49
+ setSpeakInContentLanguage(enabled: boolean): void;
50
+ getSpeakInContentLanguage(): boolean;
41
51
  private toPlainText;
42
52
  loadUtterances(contents: ReadiumSpeechUtterance[]): void;
43
53
  setVoice(voice: ReadiumSpeechVoice | string): Promise<void>;
@@ -4,7 +4,7 @@ import { ReadiumSpeechVoice } from '../voices/types';
4
4
  export declare class WebSpeechEngineProvider implements ReadiumSpeechEngineProvider {
5
5
  readonly id: string;
6
6
  readonly name: string;
7
- private engine;
7
+ private voiceEngine;
8
8
  getVoices(): Promise<ReadiumSpeechVoice[]>;
9
9
  createEngine(voice?: ReadiumSpeechVoice | string): Promise<ReadiumSpeechPlaybackEngine>;
10
10
  destroy(): Promise<void>;
package/build/engine.d.ts CHANGED
@@ -2,10 +2,13 @@ import { ReadiumSpeechPlaybackEvent, ReadiumSpeechPlaybackState } from './naviga
2
2
  import { ReadiumSpeechUtterance } from './utterance';
3
3
  import { ReadiumSpeechVoice } from './voices/types';
4
4
  export interface ReadiumSpeechPlaybackEngine {
5
+ initialize?(): Promise<unknown>;
5
6
  loadUtterances(contents: ReadiumSpeechUtterance[]): void;
6
7
  setVoice(voice: ReadiumSpeechVoice | string): void;
7
8
  getCurrentVoice(): ReadiumSpeechVoice | null;
8
9
  getAvailableVoices(): Promise<ReadiumSpeechVoice[]>;
10
+ setSpeakInContentLanguage(enabled: boolean): void;
11
+ getSpeakInContentLanguage(): boolean;
9
12
  speak(utteranceIndex?: number): void;
10
13
  pause(): void;
11
14
  resume(): void;