@readium/navigator 2.11.1 → 2.12.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.
Files changed (52) hide show
  1. package/dist/divina/DivinaNavigator.js +1 -1
  2. package/dist/epub/EpubNavigator.js +1 -1
  3. package/dist/epub/frame/FramePoolManager.js +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/preferences/guards.js +1 -1
  6. package/dist/readaloud/GuidedNavigationProvider.js +1 -0
  7. package/dist/readaloud/ReadAloudNavigator.js +1 -0
  8. package/dist/readaloud/ReadingUnit.js +1 -0
  9. package/dist/readaloud/SpeechProgress.js +1 -0
  10. package/dist/readaloud/preferences/ReadAloudDefaults.js +1 -0
  11. package/dist/readaloud/preferences/ReadAloudPreferences.js +1 -0
  12. package/dist/readaloud/preferences/ReadAloudPreferencesEditor.js +1 -0
  13. package/dist/readaloud/preferences/ReadAloudSettings.js +1 -0
  14. package/dist/webpub/WebPubFrameManager.js +1 -1
  15. package/dist/webpub/WebPubFramePoolManager.js +1 -1
  16. package/dist/webpub/WebPubNavigator.js +1 -1
  17. package/package.json +8 -6
  18. package/src/divina/DivinaNavigator.ts +1 -0
  19. package/src/epub/EpubNavigator.ts +127 -21
  20. package/src/epub/frame/FramePoolManager.ts +5 -0
  21. package/src/index.ts +1 -0
  22. package/src/preferences/guards.ts +11 -0
  23. package/src/readaloud/GuidedNavigationProvider.ts +87 -0
  24. package/src/readaloud/ReadAloudNavigator.ts +949 -0
  25. package/src/readaloud/ReadingUnit.ts +154 -0
  26. package/src/readaloud/SpeechProgress.ts +60 -0
  27. package/src/readaloud/index.ts +17 -0
  28. package/src/readaloud/preferences/ReadAloudDefaults.ts +24 -0
  29. package/src/readaloud/preferences/ReadAloudPreferences.ts +104 -0
  30. package/src/readaloud/preferences/ReadAloudPreferencesEditor.ts +115 -0
  31. package/src/readaloud/preferences/ReadAloudSettings.ts +44 -0
  32. package/src/readaloud/preferences/index.ts +4 -0
  33. package/src/webpub/WebPubFrameManager.ts +4 -0
  34. package/src/webpub/WebPubFramePoolManager.ts +5 -0
  35. package/src/webpub/WebPubNavigator.ts +114 -10
  36. package/types/src/epub/EpubNavigator.d.ts +26 -1
  37. package/types/src/epub/frame/FramePoolManager.d.ts +2 -0
  38. package/types/src/index.d.ts +1 -0
  39. package/types/src/preferences/guards.d.ts +1 -0
  40. package/types/src/readaloud/GuidedNavigationProvider.d.ts +28 -0
  41. package/types/src/readaloud/ReadAloudNavigator.d.ts +158 -0
  42. package/types/src/readaloud/ReadingUnit.d.ts +58 -0
  43. package/types/src/readaloud/SpeechProgress.d.ts +19 -0
  44. package/types/src/readaloud/index.d.ts +6 -0
  45. package/types/src/readaloud/preferences/ReadAloudDefaults.d.ts +11 -0
  46. package/types/src/readaloud/preferences/ReadAloudPreferences.d.ts +58 -0
  47. package/types/src/readaloud/preferences/ReadAloudPreferencesEditor.d.ts +37 -0
  48. package/types/src/readaloud/preferences/ReadAloudSettings.d.ts +26 -0
  49. package/types/src/readaloud/preferences/index.d.ts +4 -0
  50. package/types/src/webpub/WebPubFrameManager.d.ts +1 -0
  51. package/types/src/webpub/WebPubFramePoolManager.d.ts +2 -0
  52. package/types/src/webpub/WebPubNavigator.d.ts +25 -2
@@ -0,0 +1,154 @@
1
+ import { GuidedNavigationObject, Layout, Link, Profile, Publication } from "@readium/shared";
2
+ import { Navigator, VisualNavigatorViewport } from "../Navigator.ts";
3
+ import { GuidedNavigationProvider } from "./GuidedNavigationProvider.ts";
4
+
5
+ /** Reading order links whose Guided Navigation objects are read as one sequence. */
6
+ export interface ReadingUnit {
7
+ links: Link[];
8
+ }
9
+
10
+ /**
11
+ * Splits the publication into reading units: the displayed spread for fixed layouts, Divina excepted,
12
+ * and a single resource otherwise. Spreads are only known to the navigator, so it's moved to find them;
13
+ * FXL navigators update their viewport synchronously, so it's read without waiting for the callback.
14
+ */
15
+ export class ReadingUnits {
16
+ private readonly spreads: boolean;
17
+
18
+ constructor(private readonly navigator: Navigator & { readonly viewport?: VisualNavigatorViewport }) {
19
+ const metadata = navigator.publication.metadata;
20
+ this.spreads = metadata.effectiveLayout === Layout.fixed && !metadata.conformsTo?.includes(Profile.DIVINA);
21
+ }
22
+
23
+ /** The unit containing `href`. */
24
+ around(href: string): ReadingUnit | undefined {
25
+ const link = this.navigator.publication.readingOrder.findWithHref(href);
26
+ if (!link) return undefined;
27
+ if (this.spreads && !this.displays(link.href)) this.navigator.go(link.locator, false, () => {});
28
+ return this.displayedFrom(link, () => true);
29
+ }
30
+
31
+ /** The first link of the unit following `unit`, without moving the navigator. */
32
+ linkAfter(unit: ReadingUnit): Link | undefined {
33
+ const index = this.indexOf(unit.links[unit.links.length - 1]);
34
+ return index < 0 ? undefined : this.navigator.publication.readingOrder.items[index + 1];
35
+ }
36
+
37
+ /** The last link of the unit preceding `unit`, without moving the navigator. */
38
+ linkBefore(unit: ReadingUnit): Link | undefined {
39
+ const index = this.indexOf(unit.links[0]);
40
+ return index <= 0 ? undefined : this.navigator.publication.readingOrder.items[index - 1];
41
+ }
42
+
43
+ after(unit: ReadingUnit): ReadingUnit | undefined {
44
+ const link = this.linkAfter(unit);
45
+ if (!link) return undefined;
46
+ const last = unit.links[unit.links.length - 1];
47
+ if (this.spreads) this.move(last, link, (cb) => this.navigator.goForward(false, cb));
48
+ return this.displayedFrom(link, displayed => this.indexOf(displayed) > this.indexOf(last));
49
+ }
50
+
51
+ before(unit: ReadingUnit): ReadingUnit | undefined {
52
+ const link = this.linkBefore(unit);
53
+ if (!link) return undefined;
54
+ const first = unit.links[0];
55
+ if (this.spreads) this.move(first, link, (cb) => this.navigator.goBackward(false, cb));
56
+ return this.displayedFrom(link, displayed => this.indexOf(displayed) < this.indexOf(first));
57
+ }
58
+
59
+ /** Whether `href` is a page of the displayed spread. */
60
+ displays(href: string): boolean {
61
+ return this.spreads && (this.navigator.viewport?.readingOrder ?? []).includes(href);
62
+ }
63
+
64
+ private move(edge: Link, link: Link, step: (cb: (ok: boolean) => void) => void) {
65
+ if (this.displays(edge.href)) step(() => {});
66
+ else this.navigator.go(link.locator, false, () => {});
67
+ }
68
+
69
+ // The displayed pages kept by `keep`, if they include `link`, otherwise `link` alone.
70
+ private displayedFrom(link: Link, keep: (displayed: Link) => boolean): ReadingUnit {
71
+ if (!this.spreads) return { links: [link] };
72
+ const links = (this.navigator.viewport?.readingOrder ?? [])
73
+ .map(href => this.navigator.publication.readingOrder.findWithHref(href))
74
+ .filter((displayed): displayed is Link => displayed !== undefined && keep(displayed))
75
+ .sort((a, b) => this.indexOf(a) - this.indexOf(b));
76
+ return links.some(displayed => displayed.href === link.href) ? { links } : { links: [link] };
77
+ }
78
+
79
+ private indexOf(link: Link): number {
80
+ return this.navigator.publication.readingOrder.findIndexWithHref(link.href);
81
+ }
82
+ }
83
+
84
+ export interface StitchedUnit {
85
+ /** The unit's Guided Navigation objects, concatenated in reading order. */
86
+ guided: GuidedNavigationObject[];
87
+ /** Links whose objects couldn't be obtained, left out of `guided`. */
88
+ failures: { link: Link; error: unknown }[];
89
+ }
90
+
91
+ const UPPER_BOUNDARY = 10;
92
+ const LOWER_BOUNDARY = 5;
93
+
94
+ /**
95
+ * Keeps the Guided Navigation objects of the resources around the unit being read,
96
+ * fetching those within LOWER_BOUNDARY reading order items and releasing those beyond UPPER_BOUNDARY.
97
+ */
98
+ export class GuidedNavigationPool {
99
+ private readonly guides = new Map<string, Promise<GuidedNavigationObject[] | undefined>>();
100
+
101
+ constructor(private readonly publication: Publication, private readonly provider: GuidedNavigationProvider) {}
102
+
103
+ /**
104
+ * Concatenates the Guided Navigation objects of every link in the unit, in reading order,
105
+ * fetching at most `concurrency` resources at once, then fetches the unit's neighbours.
106
+ */
107
+ async stitch(unit: ReadingUnit, concurrency = 6): Promise<StitchedUnit> {
108
+ const guides: (GuidedNavigationObject[] | undefined)[] = new Array(unit.links.length);
109
+ const failures: { index: number; link: Link; error: unknown }[] = [];
110
+ let next = 0;
111
+ const worker = async () => {
112
+ while (next < unit.links.length) {
113
+ const index = next++;
114
+ const link = unit.links[index];
115
+ try {
116
+ guides[index] = await this.guideFor(link);
117
+ } catch (error) {
118
+ failures.push({ index, link, error });
119
+ }
120
+ }
121
+ };
122
+ const stitched = Promise.all(Array.from({ length: Math.min(concurrency, unit.links.length) }, worker));
123
+ this.update(unit);
124
+ await stitched;
125
+ return {
126
+ guided: guides.flatMap(guide => guide ?? []),
127
+ failures: failures.sort((a, b) => a.index - b.index).map(({ link, error }) => ({ link, error })),
128
+ };
129
+ }
130
+
131
+ private update(unit: ReadingUnit) {
132
+ const items = this.publication.readingOrder.items;
133
+ const i = this.publication.readingOrder.findIndexWithHref(unit.links[0].href);
134
+ if (i < 0) return;
135
+ items.forEach((link, j) => {
136
+ const href = link.href.split("#")[0];
137
+ if (j > i + UPPER_BOUNDARY || j < i - UPPER_BOUNDARY) this.guides.delete(href);
138
+ else if (j < i + LOWER_BOUNDARY && j > i - LOWER_BOUNDARY) this.guideFor(link).catch(() => {});
139
+ });
140
+ }
141
+
142
+ private guideFor(link: Link): Promise<GuidedNavigationObject[] | undefined> {
143
+ const href = link.href.split("#")[0];
144
+ let guide = this.guides.get(href);
145
+ if (!guide) {
146
+ guide = this.provider.guideFor(link);
147
+ this.guides.set(href, guide);
148
+ guide.catch(() => {
149
+ if (this.guides.get(href) === guide) this.guides.delete(href);
150
+ });
151
+ }
152
+ return guide;
153
+ }
154
+ }
@@ -0,0 +1,60 @@
1
+ import { ReadiumSpeechUtterance } from "@readium/speech";
2
+
3
+ // Characters per second at rate 1 until a voice is measured, on the slow side so pages turn late rather than early.
4
+ const DEFAULT_SPEED = 12;
5
+ // Shorter utterances are dominated by the voice's start latency.
6
+ const MIN_MEASURED_LENGTH = 40;
7
+
8
+ /** Times the utterance being spoken, and measures each voice's speed on the utterances it finishes. */
9
+ export class SpeechProgress {
10
+ private readonly speeds = new Map<string, number>();
11
+ private utterance?: ReadiumSpeechUtterance;
12
+ private startedAt = 0;
13
+ private pausedAt?: number;
14
+
15
+ start(utterance: ReadiumSpeechUtterance) {
16
+ this.utterance = utterance;
17
+ this.startedAt = performance.now();
18
+ this.pausedAt = undefined;
19
+ }
20
+
21
+ pause() {
22
+ if (this.utterance && this.pausedAt === undefined) this.pausedAt = performance.now();
23
+ }
24
+
25
+ resume() {
26
+ if (this.pausedAt === undefined) return;
27
+ this.startedAt += performance.now() - this.pausedAt;
28
+ this.pausedAt = undefined;
29
+ }
30
+
31
+ /** Measures the speed of `voice` once it has spoken `utterance` through. */
32
+ end(utterance: ReadiumSpeechUtterance | null, voice: string, rate: number) {
33
+ const length = (utterance?.plain ?? "").length;
34
+ if (utterance === this.utterance && this.pausedAt === undefined && length >= MIN_MEASURED_LENGTH && rate > 0) {
35
+ const seconds = (performance.now() - this.startedAt) / 1000;
36
+ if (seconds > 0) this.speeds.set(voice, length / seconds / rate);
37
+ }
38
+ this.reset();
39
+ }
40
+
41
+ reset() {
42
+ this.utterance = undefined;
43
+ this.pausedAt = undefined;
44
+ }
45
+
46
+ get speaking(): boolean {
47
+ return this.utterance !== undefined && this.pausedAt === undefined;
48
+ }
49
+
50
+ /** Seconds spent speaking the current utterance. */
51
+ elapsed(): number {
52
+ if (!this.utterance) return 0;
53
+ return ((this.pausedAt ?? performance.now()) - this.startedAt) / 1000;
54
+ }
55
+
56
+ /** Characters per second of `voice` at `rate`. */
57
+ speed(voice: string, rate: number): number {
58
+ return (this.speeds.get(voice) ?? DEFAULT_SPEED) * rate;
59
+ }
60
+ }
@@ -0,0 +1,17 @@
1
+ export * from './GuidedNavigationProvider.ts';
2
+ export * from './ReadAloudNavigator.ts';
3
+ export * from './preferences/index.ts';
4
+ export {
5
+ defaultContextualizations,
6
+ skippableRoles,
7
+ filterByLanguages,
8
+ filterOutNoveltyVoices,
9
+ filterOutVeryLowQualityVoices,
10
+ getQualityValue,
11
+ groupVoicesByLanguage,
12
+ sortAlphabetically,
13
+ sortByQuality,
14
+ sortVoicesByRegions
15
+ } from "@readium/speech";
16
+ export type { ExtractionFormat, LanguageMode, ReadiumSpeechPlaybackEngine, ReadiumSpeechVoice, Segmentation, VerbosityPreset } from "@readium/speech";
17
+ export type { GndRole } from "@readium/guided-navigation";
@@ -0,0 +1,24 @@
1
+ import { DecorationStyleType } from "../../decorations/index.ts";
2
+ import { ensureBoolean, ensureDecorationStyle, ensureEnumValue } from "../../preferences/guards.ts";
3
+ import { IReadAloudPreferences, ReadAloudAutoPause, ReadAloudDecorationStyle } from "./ReadAloudPreferences.ts";
4
+
5
+ export type IReadAloudDefaults = IReadAloudPreferences;
6
+
7
+ export class ReadAloudDefaults {
8
+ /** Defaults of the speech fields, validated by speech itself. */
9
+ speech: IReadAloudPreferences;
10
+ autoPause: ReadAloudAutoPause;
11
+ speakInContentLanguage: boolean;
12
+ utteranceStyle: ReadAloudDecorationStyle;
13
+ wordStyle: ReadAloudDecorationStyle;
14
+
15
+ constructor(defaults: IReadAloudDefaults = {}) {
16
+ const { autoPause, speakInContentLanguage, utteranceStyle, wordStyle, ...speech } = defaults;
17
+ // Sentences are only joined across objects, and so across pages, in sentence segmentation.
18
+ this.speech = { segmentation: "sentence", skip: ["pagebreak"], ...speech };
19
+ this.autoPause = ensureEnumValue<ReadAloudAutoPause>(autoPause, ReadAloudAutoPause) || ReadAloudAutoPause.none;
20
+ this.speakInContentLanguage = ensureBoolean(speakInContentLanguage) ?? false;
21
+ this.utteranceStyle = ensureDecorationStyle(utteranceStyle) ?? { type: DecorationStyleType.Highlight };
22
+ this.wordStyle = ensureDecorationStyle(wordStyle) ?? { type: DecorationStyleType.Underline };
23
+ }
24
+ }
@@ -0,0 +1,104 @@
1
+ import type { GndRole } from "@readium/guided-navigation";
2
+ import { ExtractionFormat, LanguageMode, Segmentation, SpeechPreferences, VerbosityPreset } from "@readium/speech";
3
+ import { ConfigurablePreferences } from "../../preferences/Configurable.ts";
4
+ import { ensureBoolean, ensureDecorationStyle, ensureEnumValue } from "../../preferences/guards.ts";
5
+ import { BuiltinDecorationStyle, NamedDecorationStyle } from "../../decorations/index.ts";
6
+
7
+ /**
8
+ * When playback pauses on its own: never, after every utterance, before every block,
9
+ * or before the first utterance starting on another page or outside the displayed spread.
10
+ */
11
+ export const ReadAloudAutoPause = {
12
+ none: "none",
13
+ utterance: "utterance",
14
+ block: "block",
15
+ page: "page",
16
+ spread: "spread",
17
+ } as const;
18
+ export type ReadAloudAutoPause = typeof ReadAloudAutoPause[keyof typeof ReadAloudAutoPause];
19
+
20
+ /** A decoration style, or `false` to not decorate at all. */
21
+ export type ReadAloudDecorationStyle = BuiltinDecorationStyle | NamedDecorationStyle | false;
22
+
23
+ export interface IReadAloudPreferences {
24
+ format?: ExtractionFormat | null,
25
+ inlineContextualization?: boolean | null,
26
+ verbosity?: VerbosityPreset | null,
27
+ skip?: GndRole[] | null,
28
+ contextualize?: GndRole[] | null,
29
+ language?: LanguageMode | null,
30
+ segmentation?: Segmentation | null,
31
+ pauseDuration?: number | null,
32
+ autoPause?: ReadAloudAutoPause | null,
33
+ speakInContentLanguage?: boolean | null,
34
+ rate?: number | null,
35
+ pitch?: number | null,
36
+ volume?: number | null,
37
+ /** Style of the utterance being spoken */
38
+ utteranceStyle?: ReadAloudDecorationStyle | null,
39
+ /** Style of the word being spoken */
40
+ wordStyle?: ReadAloudDecorationStyle | null
41
+ }
42
+
43
+ export class ReadAloudPreferences implements ConfigurablePreferences<ReadAloudPreferences> {
44
+ format?: ExtractionFormat | null;
45
+ inlineContextualization?: boolean | null;
46
+ verbosity?: VerbosityPreset | null;
47
+ skip?: GndRole[] | null;
48
+ contextualize?: GndRole[] | null;
49
+ language?: LanguageMode | null;
50
+ segmentation?: Segmentation | null;
51
+ pauseDuration?: number | null;
52
+ autoPause?: ReadAloudAutoPause | null;
53
+ speakInContentLanguage?: boolean | null;
54
+ rate?: number | null;
55
+ pitch?: number | null;
56
+ volume?: number | null;
57
+ utteranceStyle?: ReadAloudDecorationStyle | null;
58
+ wordStyle?: ReadAloudDecorationStyle | null;
59
+
60
+ constructor(preferences: IReadAloudPreferences = {}) {
61
+ const { speakInContentLanguage, ...speechPreferences } = preferences;
62
+ const speech = new SpeechPreferences({ ...speechPreferences, autoPause: undefined });
63
+ this.format = speech.format;
64
+ this.inlineContextualization = speech.inlineContextualization;
65
+ this.verbosity = speech.verbosity;
66
+ this.skip = speech.skip;
67
+ this.contextualize = speech.contextualize;
68
+ this.language = speech.language;
69
+ this.segmentation = speech.segmentation;
70
+ this.pauseDuration = speech.pauseDuration;
71
+ this.autoPause = ensureEnumValue<ReadAloudAutoPause>(preferences.autoPause, ReadAloudAutoPause);
72
+ this.speakInContentLanguage = ensureBoolean(speakInContentLanguage);
73
+ this.rate = speech.rate;
74
+ this.pitch = speech.pitch;
75
+ this.volume = speech.volume;
76
+ this.utteranceStyle = ensureDecorationStyle(preferences.utteranceStyle);
77
+ this.wordStyle = ensureDecorationStyle(preferences.wordStyle);
78
+ }
79
+
80
+ static serialize(preferences: ReadAloudPreferences): string {
81
+ const { ...properties } = preferences;
82
+ return JSON.stringify(properties);
83
+ }
84
+
85
+ static deserialize(preferences: string): ReadAloudPreferences | null {
86
+ try {
87
+ const parsedPreferences = JSON.parse(preferences);
88
+ return new ReadAloudPreferences(parsedPreferences);
89
+ } catch (error) {
90
+ console.error("Failed to deserialize preferences:", error);
91
+ return null;
92
+ }
93
+ }
94
+
95
+ merging(other: ReadAloudPreferences): ReadAloudPreferences {
96
+ const merged: IReadAloudPreferences = { ...this };
97
+ for (const key of Object.keys(other) as (keyof IReadAloudPreferences)[]) {
98
+ if (other[key] !== undefined) {
99
+ (merged as Record<string, unknown>)[key] = other[key];
100
+ }
101
+ }
102
+ return new ReadAloudPreferences(merged);
103
+ }
104
+ }
@@ -0,0 +1,115 @@
1
+ import { Layout } from "@readium/shared";
2
+ import { SpeechPreferences, SpeechPreferencesEditor, SpeechSettings } from "@readium/speech";
3
+ import { IPreferencesEditor } from "../../preferences/PreferencesEditor.ts";
4
+ import { BooleanPreference, EnumPreference, Preference } from "../../preferences/Preference.ts";
5
+ import { ReadAloudAutoPause, ReadAloudDecorationStyle, ReadAloudPreferences } from "./ReadAloudPreferences.ts";
6
+ import { ReadAloudSettings } from "./ReadAloudSettings.ts";
7
+
8
+ /**
9
+ * Edits read-aloud preferences, speech's own fields through speech's editor.
10
+ */
11
+ export class ReadAloudPreferencesEditor implements IPreferencesEditor {
12
+ private speech: SpeechPreferencesEditor;
13
+ private own: ReadAloudPreferences;
14
+ private settings: ReadAloudSettings;
15
+ private layout: Layout;
16
+
17
+ /**
18
+ * @param layout How the publication is displayed: `Layout.reflowable` for columns, `Layout.scrolled` when scrolled.
19
+ */
20
+ constructor(initialPreferences: ReadAloudPreferences, settings: ReadAloudSettings, speechSettings: SpeechSettings, layout: Layout) {
21
+ const { speakInContentLanguage, ...speechPreferences } = initialPreferences;
22
+ this.speech = new SpeechPreferencesEditor(new SpeechPreferences({ ...speechPreferences, autoPause: undefined }), speechSettings);
23
+ this.own = new ReadAloudPreferences({
24
+ autoPause: initialPreferences.autoPause,
25
+ speakInContentLanguage,
26
+ utteranceStyle: initialPreferences.utteranceStyle,
27
+ wordStyle: initialPreferences.wordStyle
28
+ });
29
+ this.settings = settings;
30
+ this.layout = layout;
31
+ }
32
+
33
+ get preferences(): ReadAloudPreferences {
34
+ return new ReadAloudPreferences({
35
+ ...this.speech.preferences,
36
+ autoPause: this.own.autoPause,
37
+ speakInContentLanguage: this.own.speakInContentLanguage,
38
+ utteranceStyle: this.own.utteranceStyle,
39
+ wordStyle: this.own.wordStyle
40
+ });
41
+ }
42
+
43
+ clear(): void {
44
+ this.speech.clear();
45
+ this.own = new ReadAloudPreferences({
46
+ autoPause: null,
47
+ speakInContentLanguage: null,
48
+ utteranceStyle: null,
49
+ wordStyle: null
50
+ });
51
+ }
52
+
53
+ private updatePreference<K extends keyof ReadAloudPreferences>(key: K, value: ReadAloudPreferences[K]) {
54
+ this.own[key] = value;
55
+ }
56
+
57
+ get format() { return this.speech.format; }
58
+ get inlineContextualization() { return this.speech.inlineContextualization; }
59
+ get verbosity() { return this.speech.verbosity; }
60
+ get skip() { return this.speech.skip; }
61
+ get contextualize() { return this.speech.contextualize; }
62
+ get language() { return this.speech.language; }
63
+ get segmentation() { return this.speech.segmentation; }
64
+ get pauseDuration() { return this.speech.pauseDuration; }
65
+ get rate() { return this.speech.rate; }
66
+ get pitch() { return this.speech.pitch; }
67
+ get volume() { return this.speech.volume; }
68
+
69
+ get autoPause(): EnumPreference<ReadAloudAutoPause> {
70
+ return new EnumPreference<ReadAloudAutoPause>({
71
+ initialValue: this.own.autoPause,
72
+ effectiveValue: this.settings.autoPause,
73
+ isEffective: this.layout === Layout.fixed || this.settings.autoPause !== ReadAloudAutoPause.spread &&
74
+ (this.layout === Layout.reflowable || this.settings.autoPause !== ReadAloudAutoPause.page),
75
+ onChange: (newValue) => {
76
+ this.updatePreference("autoPause", newValue ?? null);
77
+ },
78
+ supportedValues: Object.values(ReadAloudAutoPause)
79
+ .filter(value => value !== ReadAloudAutoPause.spread || this.layout === Layout.fixed)
80
+ });
81
+ }
82
+
83
+ get speakInContentLanguage(): BooleanPreference {
84
+ return new BooleanPreference({
85
+ initialValue: this.own.speakInContentLanguage,
86
+ effectiveValue: this.settings.speakInContentLanguage,
87
+ isEffective: this.settings.language !== "none",
88
+ onChange: (newValue) => {
89
+ this.updatePreference("speakInContentLanguage", newValue ?? null);
90
+ }
91
+ });
92
+ }
93
+
94
+ get utteranceStyle(): Preference<ReadAloudDecorationStyle> {
95
+ return new Preference<ReadAloudDecorationStyle>({
96
+ initialValue: this.own.utteranceStyle,
97
+ effectiveValue: this.settings.utteranceStyle,
98
+ isEffective: true,
99
+ onChange: (newValue) => {
100
+ this.updatePreference("utteranceStyle", newValue ?? null);
101
+ }
102
+ });
103
+ }
104
+
105
+ get wordStyle(): Preference<ReadAloudDecorationStyle> {
106
+ return new Preference<ReadAloudDecorationStyle>({
107
+ initialValue: this.own.wordStyle,
108
+ effectiveValue: this.settings.wordStyle,
109
+ isEffective: true,
110
+ onChange: (newValue) => {
111
+ this.updatePreference("wordStyle", newValue ?? null);
112
+ }
113
+ });
114
+ }
115
+ }
@@ -0,0 +1,44 @@
1
+ import type { GndRole } from "@readium/guided-navigation";
2
+ import { ExtractionFormat, LanguageMode, Segmentation, SpeechSettings, VerbosityPreset } from "@readium/speech";
3
+ import { ConfigurableSettings } from "../../preferences/Configurable.ts";
4
+ import { ReadAloudDefaults } from "./ReadAloudDefaults.ts";
5
+ import { ReadAloudAutoPause, ReadAloudDecorationStyle, ReadAloudPreferences } from "./ReadAloudPreferences.ts";
6
+
7
+ export class ReadAloudSettings implements ConfigurableSettings {
8
+ format: ExtractionFormat;
9
+ inlineContextualization: boolean;
10
+ verbosity: VerbosityPreset;
11
+ skip: GndRole[];
12
+ contextualize: GndRole[];
13
+ language: LanguageMode;
14
+ segmentation: Segmentation;
15
+ pauseDuration: number;
16
+ autoPause: ReadAloudAutoPause;
17
+ speakInContentLanguage: boolean;
18
+ rate: number;
19
+ pitch: number;
20
+ volume: number;
21
+ utteranceStyle: ReadAloudDecorationStyle;
22
+ wordStyle: ReadAloudDecorationStyle;
23
+
24
+ /**
25
+ * @param speech The settings speech resolved for its own fields.
26
+ */
27
+ constructor(speech: SpeechSettings, preferences: ReadAloudPreferences, defaults: ReadAloudDefaults) {
28
+ this.format = speech.format;
29
+ this.inlineContextualization = speech.inlineContextualization;
30
+ this.verbosity = speech.verbosity;
31
+ this.skip = speech.skip;
32
+ this.contextualize = speech.contextualize;
33
+ this.language = speech.language;
34
+ this.segmentation = speech.segmentation;
35
+ this.pauseDuration = speech.pauseDuration;
36
+ this.autoPause = preferences.autoPause ?? defaults.autoPause;
37
+ this.speakInContentLanguage = preferences.speakInContentLanguage ?? defaults.speakInContentLanguage;
38
+ this.rate = speech.rate;
39
+ this.pitch = speech.pitch;
40
+ this.volume = speech.volume;
41
+ this.utteranceStyle = preferences.utteranceStyle ?? defaults.utteranceStyle;
42
+ this.wordStyle = preferences.wordStyle ?? defaults.wordStyle;
43
+ }
44
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./ReadAloudDefaults.ts";
2
+ export * from "./ReadAloudPreferences.ts";
3
+ export * from "./ReadAloudPreferencesEditor.ts";
4
+ export * from "./ReadAloudSettings.ts";
@@ -232,6 +232,10 @@ export class WebPubFrameManager {
232
232
  return this.frame.getBoundingClientRect();
233
233
  }
234
234
 
235
+ get isDestroyed() {
236
+ return this.destroyed;
237
+ }
238
+
235
239
  get window() {
236
240
  if(this.destroyed || !this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
237
241
  return this.frame.contentWindow;
@@ -252,6 +252,11 @@ export class WebPubFramePoolManager {
252
252
  return [this._currentFrame];
253
253
  }
254
254
 
255
+ /** The pooled frame of `href`, whether or not it's shown. */
256
+ _frameFor(href: string): WebPubFrameManager | undefined {
257
+ return this.pool.get(href);
258
+ }
259
+
255
260
  get currentBounds(): DOMRect {
256
261
  const ret = {
257
262
  x: 0,