@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.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 +658 -714
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +1 -21
- package/dist/attempt/TestSession.js +16 -102
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
- package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +33 -6
- package/dist/index.d.ts +25 -50
- package/dist/index.js +15 -45
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
- package/dist/policy/core/ToolPolicyEngine.js +203 -56
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +95 -74
- package/dist/policy/core/decision-types.d.ts +98 -55
- package/dist/policy/core/feature-decision.d.ts +28 -19
- package/dist/policy/core/feature-decision.js +15 -11
- package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
- package/dist/policy/core/pnp-policy-inputs.js +40 -88
- package/dist/policy/core/provenance.d.ts +10 -5
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
- package/dist/policy/sources/PnpPolicySource.js +133 -132
- package/dist/runtime/SectionControllerBinding.d.ts +283 -0
- package/dist/runtime/SectionControllerBinding.js +456 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
- package/dist/runtime/adapter/subscriber-fanout.js +2 -6
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
- package/dist/runtime/composition-emit-scheduler.js +10 -16
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +13 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
- package/dist/runtime/core/SectionEngineCore.js +2 -6
- package/dist/runtime/core/cohort.d.ts +1 -6
- package/dist/runtime/core/cohort.js +1 -10
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +17 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +74 -53
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +22 -14
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +194 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -14
- package/dist/runtime/tool-host-contract.js +12 -12
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +42 -67
- package/dist/services/ElementToolStateStore.js +65 -71
- package/dist/services/HighlightCoordinator.d.ts +28 -40
- package/dist/services/HighlightCoordinator.js +459 -381
- package/dist/services/RangeSerializer.d.ts +11 -33
- package/dist/services/RangeSerializer.js +88 -59
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +159 -119
- package/dist/services/TTSService.js +937 -822
- package/dist/services/ToolCoordinator.d.ts +42 -102
- package/dist/services/ToolCoordinator.js +178 -256
- package/dist/services/ToolRegistry.d.ts +152 -133
- package/dist/services/ToolRegistry.js +192 -178
- package/dist/services/ToolkitCoordinator.d.ts +233 -237
- package/dist/services/ToolkitCoordinator.js +615 -557
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +51 -13
- package/dist/services/framework-error.js +19 -20
- package/dist/services/interfaces.d.ts +189 -184
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +80 -0
- package/dist/services/spoken-audio-cards.d.ts +2 -6
- package/dist/services/spoken-audio-cards.js +2 -6
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +9 -12
- package/dist/services/tool-config-validation.js +47 -87
- package/dist/services/tool-context.d.ts +7 -31
- package/dist/services/tool-context.js +99 -65
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +5 -5
- package/dist/services/tool-icons.js +6 -5
- package/dist/services/tool-instance-id.d.ts +8 -7
- package/dist/services/tool-instance-id.js +12 -24
- package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
- package/dist/services/tool-providers/TTSToolProvider.js +32 -85
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
- package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
- package/dist/services/tool-request.d.ts +31 -12
- package/dist/services/tool-request.js +31 -16
- package/dist/services/toolbar-decided-tools.d.ts +16 -0
- package/dist/services/toolbar-decided-tools.js +29 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +33 -29
- package/dist/services/tools-config-normalizer.js +26 -28
- package/dist/services/tts/browser-provider.d.ts +39 -6
- package/dist/services/tts/browser-provider.js +280 -160
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +64 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +42 -47
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +12 -1
- package/dist/services/tts/math-speech.js +54 -18
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +52 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +4 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +12 -0
- package/dist/services/tts/text-segmentation.js +60 -10
- package/dist/services/tts-runtime-config.d.ts +54 -26
- package/dist/services/tts-runtime-config.js +27 -112
- package/dist/tools/content-capability-resolution.d.ts +9 -11
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +59 -0
- package/dist/tools/registration.js +55 -0
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +45 -50
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +15 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -3,12 +3,85 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Text-to-speech provider using the browser's Web Speech API.
|
|
5
5
|
* Works in all modern browsers that support SpeechSynthesis.
|
|
6
|
-
*
|
|
7
|
-
* Part of PIE Assessment Toolkit.
|
|
8
6
|
*/
|
|
7
|
+
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
8
|
+
import { extractSpokenText, isSsmlDocument } from "./ssml/spoken-text.js";
|
|
9
9
|
import { segmentSentences as segmentTextToSentences } from "./text-segmentation.js";
|
|
10
|
+
const logger = createPieLogger("browser-tts-provider", isTtsDebugEnabled);
|
|
10
11
|
const NATIVE_START_TIMEOUT_MS = 5_000;
|
|
11
12
|
const VOICE_INVENTORY_TIMEOUT_MS = 2_000;
|
|
13
|
+
const MAX_CHUNK_LENGTH = 260;
|
|
14
|
+
// A longer reported word length falls back to the inferred one.
|
|
15
|
+
const MAX_TRUSTED_REPORTED_WORD_LENGTH = 80;
|
|
16
|
+
// The provider options the toolkit and this provider read. Every other option,
|
|
17
|
+
// and `voice`, belongs to the provider it was configured for: a Polly voice such
|
|
18
|
+
// as "Joanna" resolves to no browser voice, and an unresolved voice fails speak.
|
|
19
|
+
const PORTABLE_PROVIDER_OPTIONS = [
|
|
20
|
+
"__pieTelemetry",
|
|
21
|
+
"contentLanguage",
|
|
22
|
+
"highlightMode",
|
|
23
|
+
"locale",
|
|
24
|
+
"mathSpeech",
|
|
25
|
+
"segmenter",
|
|
26
|
+
"structuralPauses",
|
|
27
|
+
"textNormalization",
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* The part of another provider's configuration that keeps its meaning on
|
|
31
|
+
* browser speech. Every path that replaces a provider with browser speech
|
|
32
|
+
* starts it with this.
|
|
33
|
+
*/
|
|
34
|
+
export const browserFallbackConfig = (config) => {
|
|
35
|
+
const fallback = {};
|
|
36
|
+
if (config.rate !== undefined)
|
|
37
|
+
fallback.rate = config.rate;
|
|
38
|
+
if (config.pitch !== undefined)
|
|
39
|
+
fallback.pitch = config.pitch;
|
|
40
|
+
if (config.mathTokenHighlighting !== undefined) {
|
|
41
|
+
fallback.mathTokenHighlighting = config.mathTokenHighlighting;
|
|
42
|
+
}
|
|
43
|
+
const options = config.providerOptions;
|
|
44
|
+
if (options && typeof options === "object") {
|
|
45
|
+
const portable = {};
|
|
46
|
+
for (const key of PORTABLE_PROVIDER_OPTIONS) {
|
|
47
|
+
if (options[key] !== undefined)
|
|
48
|
+
portable[key] = options[key];
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(portable).length > 0)
|
|
51
|
+
fallback.providerOptions = portable;
|
|
52
|
+
}
|
|
53
|
+
return fallback;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The Web Speech API reads SSML tags aloud, so a `<speak>` document is voiced
|
|
57
|
+
* as its spoken text. Boundary offsets still index the text as sent: that is
|
|
58
|
+
* the space the highlight pipeline resolves a raw-SSML chunk in.
|
|
59
|
+
*/
|
|
60
|
+
const toUtteranceText = (text) => {
|
|
61
|
+
if (!isSsmlDocument(text)) {
|
|
62
|
+
return { text, sourceOffset: (offset) => offset };
|
|
63
|
+
}
|
|
64
|
+
const { spokenText, rawToSpokenOffsetMap } = extractSpokenText(text);
|
|
65
|
+
const spokenToRaw = [];
|
|
66
|
+
for (const [raw, spoken] of rawToSpokenOffsetMap) {
|
|
67
|
+
const known = spokenToRaw[spoken];
|
|
68
|
+
if (known === undefined || raw < known)
|
|
69
|
+
spokenToRaw[spoken] = raw;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
text: spokenText,
|
|
73
|
+
// Alias text (`<sub alias>`) has no raw offset; it takes the nearest
|
|
74
|
+
// preceding one.
|
|
75
|
+
sourceOffset: (offset) => {
|
|
76
|
+
for (let index = offset; index >= 0; index--) {
|
|
77
|
+
const raw = spokenToRaw[index];
|
|
78
|
+
if (raw !== undefined)
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
12
85
|
const normalizeLanguageCode = (value) => String(value || "")
|
|
13
86
|
.trim()
|
|
14
87
|
.toLowerCase();
|
|
@@ -18,19 +91,28 @@ const browserLanguage = () => {
|
|
|
18
91
|
: "";
|
|
19
92
|
return normalizeLanguageCode(navigatorLanguage || "en-US");
|
|
20
93
|
};
|
|
21
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Whether a voice speaks `contentLanguage`, or the browser's own language when
|
|
96
|
+
* the content names none: the same language, or the same primary subtag.
|
|
97
|
+
*/
|
|
98
|
+
export const browserVoiceMatchesLanguage = (voice, contentLanguage) => {
|
|
99
|
+
const language = normalizeLanguageCode(contentLanguage) || browserLanguage();
|
|
100
|
+
const languagePrefix = language.split("-")[0] || "en";
|
|
101
|
+
const voiceLanguage = normalizeLanguageCode(voice.lang);
|
|
102
|
+
return (voiceLanguage === language || voiceLanguage.startsWith(`${languagePrefix}-`));
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The voice to read with: the configured one, else one for `contentLanguage`
|
|
106
|
+
* (the language of the content being read), else one for the browser's own
|
|
107
|
+
* language.
|
|
108
|
+
*/
|
|
109
|
+
export const findBrowserVoice = (voices, preferredVoice, contentLanguage) => {
|
|
22
110
|
if (preferredVoice) {
|
|
23
111
|
return (voices.find((voice) => voice.voiceURI === preferredVoice) ||
|
|
24
112
|
voices.find((voice) => voice.name === preferredVoice) ||
|
|
25
113
|
null);
|
|
26
114
|
}
|
|
27
|
-
const
|
|
28
|
-
const languagePrefix = language.split("-")[0] || "en";
|
|
29
|
-
const matchesLanguage = (voice) => {
|
|
30
|
-
const voiceLanguage = normalizeLanguageCode(voice.lang);
|
|
31
|
-
return (voiceLanguage === language ||
|
|
32
|
-
voiceLanguage.startsWith(`${languagePrefix}-`));
|
|
33
|
-
};
|
|
115
|
+
const matchesLanguage = (voice) => browserVoiceMatchesLanguage(voice, contentLanguage);
|
|
34
116
|
const ranked = [
|
|
35
117
|
(voice) => voice.localService && matchesLanguage(voice),
|
|
36
118
|
(voice) => voice.default && matchesLanguage(voice),
|
|
@@ -45,6 +127,64 @@ const findBrowserVoice = (voices, preferredVoice) => {
|
|
|
45
127
|
}
|
|
46
128
|
return voices[0] || null;
|
|
47
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* Waits for `synth` to publish its voice inventory, which browsers fill
|
|
132
|
+
* asynchronously after page load. Resolves `true` once `getVoices()` is
|
|
133
|
+
* non-empty, at once when it already is; `false` after `timeoutMs`, once
|
|
134
|
+
* `isCurrent` reports the wait superseded, or when `synth` offers no
|
|
135
|
+
* `voiceschanged` channel to wait on. A previous `onvoiceschanged` handler
|
|
136
|
+
* keeps running and is restored afterwards.
|
|
137
|
+
*/
|
|
138
|
+
export const waitForBrowserVoices = (synth, timeoutMs, isCurrent = () => true) => {
|
|
139
|
+
if (synth.getVoices().length > 0)
|
|
140
|
+
return Promise.resolve(true);
|
|
141
|
+
const canUseEventTarget = typeof synth.addEventListener === "function" &&
|
|
142
|
+
typeof synth.removeEventListener === "function";
|
|
143
|
+
if (!canUseEventTarget && !("onvoiceschanged" in synth)) {
|
|
144
|
+
return Promise.resolve(false);
|
|
145
|
+
}
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const previousHandler = canUseEventTarget ? null : synth.onvoiceschanged;
|
|
148
|
+
let settled = false;
|
|
149
|
+
const finish = (voicesAvailable) => {
|
|
150
|
+
if (settled)
|
|
151
|
+
return;
|
|
152
|
+
settled = true;
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
if (canUseEventTarget) {
|
|
155
|
+
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
156
|
+
}
|
|
157
|
+
else if (synth.onvoiceschanged === propertyHandler) {
|
|
158
|
+
synth.onvoiceschanged = previousHandler;
|
|
159
|
+
}
|
|
160
|
+
resolve(voicesAvailable);
|
|
161
|
+
};
|
|
162
|
+
const inspectVoiceInventory = () => {
|
|
163
|
+
if (!isCurrent())
|
|
164
|
+
finish(false);
|
|
165
|
+
else if (synth.getVoices().length > 0)
|
|
166
|
+
finish(true);
|
|
167
|
+
};
|
|
168
|
+
const onVoicesChanged = () => inspectVoiceInventory();
|
|
169
|
+
const propertyHandler = (event) => {
|
|
170
|
+
try {
|
|
171
|
+
previousHandler?.call(synth, event);
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
inspectVoiceInventory();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const timeout = setTimeout(() => finish(false), timeoutMs);
|
|
178
|
+
if (canUseEventTarget) {
|
|
179
|
+
synth.addEventListener("voiceschanged", onVoicesChanged);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
synth.onvoiceschanged = propertyHandler;
|
|
183
|
+
}
|
|
184
|
+
// Close the getVoices()/listener-registration race without polling.
|
|
185
|
+
inspectVoiceInventory();
|
|
186
|
+
});
|
|
187
|
+
};
|
|
48
188
|
const shouldAssignBrowserVoice = (voice) => !voice.default;
|
|
49
189
|
/**
|
|
50
190
|
* Browser TTS Provider
|
|
@@ -53,31 +193,26 @@ const shouldAssignBrowserVoice = (voice) => !voice.default;
|
|
|
53
193
|
*/
|
|
54
194
|
export class BrowserTTSProvider {
|
|
55
195
|
providerId = "browser";
|
|
56
|
-
providerName = "Browser Speech Synthesis";
|
|
57
|
-
version = "1.0.0";
|
|
58
196
|
async initialize(config) {
|
|
59
197
|
// SSR guard
|
|
60
198
|
if (typeof window === "undefined") {
|
|
61
199
|
throw new Error("BrowserTTSProvider requires browser environment");
|
|
62
200
|
}
|
|
63
|
-
// Check browser support
|
|
64
201
|
if (!("speechSynthesis" in window)) {
|
|
65
202
|
throw new Error("Browser does not support Speech Synthesis API");
|
|
66
203
|
}
|
|
204
|
+
// Only a configured voice needs the inventory; waiting otherwise would
|
|
205
|
+
// hold readiness on a browser that publishes no voices.
|
|
206
|
+
if (typeof config.voice === "string" && config.voice.trim()) {
|
|
207
|
+
await waitForBrowserVoices(window.speechSynthesis, VOICE_INVENTORY_TIMEOUT_MS);
|
|
208
|
+
}
|
|
67
209
|
return new BrowserTTSProviderImpl(config);
|
|
68
210
|
}
|
|
69
|
-
supportsFeature(feature) {
|
|
70
|
-
// Browser supports all TTS features
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
211
|
getCapabilities() {
|
|
74
212
|
return {
|
|
75
|
-
supportsPause: true,
|
|
76
|
-
supportsResume: true,
|
|
77
213
|
supportsWordBoundary: true,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
supportsPitchControl: true,
|
|
214
|
+
// Boundary events depend on the voice: several network voices send none.
|
|
215
|
+
defaultHighlightMode: "sentence",
|
|
81
216
|
// The Web Speech API voices plain text only; SSML tags are read aloud.
|
|
82
217
|
supportsSSML: false,
|
|
83
218
|
};
|
|
@@ -95,99 +230,45 @@ class BrowserTTSProviderImpl {
|
|
|
95
230
|
utterance = null;
|
|
96
231
|
config = null;
|
|
97
232
|
_isPlaying = false;
|
|
233
|
+
// The run's pause, which outlasts the utterance it paused: a pause between
|
|
234
|
+
// utterances holds the next one until resume.
|
|
98
235
|
_isPaused = false;
|
|
99
236
|
speakRunId = 0;
|
|
100
|
-
|
|
237
|
+
// The run speaking now, between its utterances as well as during them.
|
|
238
|
+
activeRunId = null;
|
|
239
|
+
releasePauseHold = null;
|
|
101
240
|
settlePendingChunk = null;
|
|
241
|
+
settlePendingGap = null;
|
|
102
242
|
onPlaybackStart = undefined;
|
|
103
243
|
constructor(config) {
|
|
104
244
|
this.config = config;
|
|
105
245
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (canUseEventTarget) {
|
|
121
|
-
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
122
|
-
}
|
|
123
|
-
else if (synth.onvoiceschanged === propertyHandler) {
|
|
124
|
-
synth.onvoiceschanged = previousHandler;
|
|
125
|
-
}
|
|
126
|
-
if (pendingSettlement &&
|
|
127
|
-
this.settlePendingVoiceWait === pendingSettlement) {
|
|
128
|
-
this.settlePendingVoiceWait = null;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
const finish = (voicesAvailable, error) => {
|
|
132
|
-
if (settled)
|
|
133
|
-
return;
|
|
134
|
-
settled = true;
|
|
135
|
-
cleanup();
|
|
136
|
-
if (error) {
|
|
137
|
-
reject(error);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
resolve(voicesAvailable);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
const inspectVoiceInventory = () => {
|
|
144
|
-
if (runId !== this.speakRunId) {
|
|
145
|
-
finish(false);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (synth.getVoices().length > 0)
|
|
149
|
-
finish(true);
|
|
150
|
-
};
|
|
151
|
-
const onVoicesChanged = () => inspectVoiceInventory();
|
|
152
|
-
const propertyHandler = (event) => {
|
|
153
|
-
try {
|
|
154
|
-
previousHandler?.call(synth, event);
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
inspectVoiceInventory();
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
pendingSettlement = () => finish(false);
|
|
161
|
-
this.settlePendingVoiceWait = pendingSettlement;
|
|
162
|
-
timeout = setTimeout(() => finish(false, new Error(`Configured browser voice "${configuredVoice}" could not be resolved because the browser did not publish its voice inventory within ${VOICE_INVENTORY_TIMEOUT_MS / 1_000} seconds.`)), VOICE_INVENTORY_TIMEOUT_MS);
|
|
163
|
-
if (canUseEventTarget) {
|
|
164
|
-
synth.addEventListener("voiceschanged", onVoicesChanged);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
synth.onvoiceschanged = propertyHandler;
|
|
168
|
-
}
|
|
169
|
-
// Close the getVoices()/listener-registration race without polling.
|
|
170
|
-
inspectVoiceInventory();
|
|
171
|
-
});
|
|
246
|
+
/** Whether run `runId` is still current once any pause between utterances ends. */
|
|
247
|
+
async waitWhilePaused(runId) {
|
|
248
|
+
while (this._isPaused && runId === this.speakRunId) {
|
|
249
|
+
await new Promise((resolve) => {
|
|
250
|
+
this.releasePauseHold = resolve;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return runId === this.speakRunId;
|
|
254
|
+
}
|
|
255
|
+
releaseHold() {
|
|
256
|
+
const release = this.releasePauseHold;
|
|
257
|
+
this.releasePauseHold = null;
|
|
258
|
+
release?.();
|
|
172
259
|
}
|
|
173
|
-
|
|
260
|
+
/** The voice to read with; `initialize()` already waited for the inventory. */
|
|
261
|
+
resolveBrowserVoice() {
|
|
174
262
|
const configuredVoice = typeof this.config?.voice === "string" ? this.config.voice.trim() : "";
|
|
175
|
-
|
|
263
|
+
const voices = speechSynthesis.getVoices();
|
|
176
264
|
if (configuredVoice && voices.length === 0) {
|
|
177
|
-
|
|
178
|
-
if (!voicesAvailable || runId !== this.speakRunId) {
|
|
179
|
-
return { shouldContinue: false, voice: null };
|
|
180
|
-
}
|
|
181
|
-
voices = speechSynthesis.getVoices();
|
|
265
|
+
throw new Error(`Configured browser voice "${configuredVoice}" could not be resolved because the browser did not publish its voice inventory within ${VOICE_INVENTORY_TIMEOUT_MS / 1_000} seconds.`);
|
|
182
266
|
}
|
|
183
|
-
|
|
184
|
-
return { shouldContinue: false, voice: null };
|
|
185
|
-
}
|
|
186
|
-
const voice = findBrowserVoice(voices, configuredVoice || undefined);
|
|
267
|
+
const voice = findBrowserVoice(voices, configuredVoice || undefined, this.getContentLocale());
|
|
187
268
|
if (configuredVoice && !voice) {
|
|
188
269
|
throw new Error(`Configured browser voice "${configuredVoice}" is unavailable. Select a voice exposed by this browser using its voiceURI or name.`);
|
|
189
270
|
}
|
|
190
|
-
return
|
|
271
|
+
return voice;
|
|
191
272
|
}
|
|
192
273
|
async speak(text) {
|
|
193
274
|
if (!this.config) {
|
|
@@ -196,19 +277,24 @@ class BrowserTTSProviderImpl {
|
|
|
196
277
|
// Invalidate any in-flight run and cancel current utterance.
|
|
197
278
|
this.stop();
|
|
198
279
|
const runId = this.speakRunId;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
280
|
+
this.activeRunId = runId;
|
|
281
|
+
try {
|
|
282
|
+
const voice = this.resolveBrowserVoice();
|
|
283
|
+
const utterance = toUtteranceText(text);
|
|
284
|
+
const chunks = this.splitIntoChunks(utterance.text);
|
|
285
|
+
for (const chunk of chunks) {
|
|
286
|
+
if (this._isPaused && !(await this.waitWhilePaused(runId)))
|
|
287
|
+
break;
|
|
288
|
+
const shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId, voice, utterance.sourceOffset);
|
|
289
|
+
if (!shouldContinue) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
210
292
|
}
|
|
211
293
|
}
|
|
294
|
+
finally {
|
|
295
|
+
if (this.activeRunId === runId)
|
|
296
|
+
this.activeRunId = null;
|
|
297
|
+
}
|
|
212
298
|
}
|
|
213
299
|
async speakSegments(segments) {
|
|
214
300
|
if (!this.config) {
|
|
@@ -216,34 +302,48 @@ class BrowserTTSProviderImpl {
|
|
|
216
302
|
}
|
|
217
303
|
this.stop();
|
|
218
304
|
const runId = this.speakRunId;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (runId !== this.speakRunId)
|
|
224
|
-
break;
|
|
225
|
-
const chunks = this.splitIntoChunks(segment.text);
|
|
226
|
-
for (const chunk of chunks) {
|
|
305
|
+
this.activeRunId = runId;
|
|
306
|
+
try {
|
|
307
|
+
const voice = this.resolveBrowserVoice();
|
|
308
|
+
for (const segment of segments) {
|
|
227
309
|
if (runId !== this.speakRunId)
|
|
228
310
|
break;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
311
|
+
const utterance = toUtteranceText(segment.text);
|
|
312
|
+
const chunks = this.splitIntoChunks(utterance.text);
|
|
313
|
+
for (const chunk of chunks) {
|
|
314
|
+
if (this._isPaused && !(await this.waitWhilePaused(runId)))
|
|
315
|
+
break;
|
|
316
|
+
const shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId, voice, (offset) => segment.startOffset + utterance.sourceOffset(offset));
|
|
317
|
+
if (!shouldContinue)
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
const pauseMsAfter = Math.max(0, Number(segment.pauseMsAfter || 0));
|
|
321
|
+
if (pauseMsAfter > 0 && runId === this.speakRunId) {
|
|
322
|
+
const shouldContinue = await this.waitForPause(pauseMsAfter, runId);
|
|
323
|
+
if (!shouldContinue)
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
238
326
|
}
|
|
239
327
|
}
|
|
328
|
+
finally {
|
|
329
|
+
if (this.activeRunId === runId)
|
|
330
|
+
this.activeRunId = null;
|
|
331
|
+
}
|
|
240
332
|
}
|
|
241
333
|
async waitForPause(pauseMs, runId) {
|
|
242
|
-
await new Promise((resolve) =>
|
|
334
|
+
await new Promise((resolve) => {
|
|
335
|
+
const settle = () => {
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
if (this.settlePendingGap === settle)
|
|
338
|
+
this.settlePendingGap = null;
|
|
339
|
+
resolve();
|
|
340
|
+
};
|
|
341
|
+
const timer = setTimeout(settle, pauseMs);
|
|
342
|
+
this.settlePendingGap = settle;
|
|
343
|
+
});
|
|
243
344
|
return runId === this.speakRunId;
|
|
244
345
|
}
|
|
245
346
|
splitIntoChunks(text) {
|
|
246
|
-
const MAX_CHUNK_LENGTH = 260;
|
|
247
347
|
if (text.length <= MAX_CHUNK_LENGTH) {
|
|
248
348
|
return [{ text, offset: 0 }];
|
|
249
349
|
}
|
|
@@ -277,13 +377,19 @@ class BrowserTTSProviderImpl {
|
|
|
277
377
|
}
|
|
278
378
|
return chunks.length ? chunks : [{ text, offset: 0 }];
|
|
279
379
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
380
|
+
/**
|
|
381
|
+
* The language of the content being read, which the toolkit sets per speak as
|
|
382
|
+
* `providerOptions.contentLanguage` when the content or the host names one.
|
|
383
|
+
*/
|
|
384
|
+
getContentLocale() {
|
|
385
|
+
const language = this.config?.providerOptions?.contentLanguage;
|
|
386
|
+
return typeof language === "string" && language.trim()
|
|
387
|
+
? language.trim()
|
|
388
|
+
: undefined;
|
|
283
389
|
}
|
|
284
390
|
getSegmentationPolicy() {
|
|
285
|
-
const providerOptions =
|
|
286
|
-
const segmenter =
|
|
391
|
+
const providerOptions = this.config?.providerOptions || {};
|
|
392
|
+
const segmenter = providerOptions.segmenter || {};
|
|
287
393
|
const mode = segmenter.mode;
|
|
288
394
|
const useSegmenter = mode !== "regexOnly";
|
|
289
395
|
const locale = typeof segmenter.locale === "string" && segmenter.locale.trim().length > 0
|
|
@@ -294,7 +400,6 @@ class BrowserTTSProviderImpl {
|
|
|
294
400
|
: undefined;
|
|
295
401
|
return {
|
|
296
402
|
useSentenceSegmenter: useSegmenter,
|
|
297
|
-
useWordSegmenter: useSegmenter,
|
|
298
403
|
locale,
|
|
299
404
|
};
|
|
300
405
|
}
|
|
@@ -311,7 +416,11 @@ class BrowserTTSProviderImpl {
|
|
|
311
416
|
const match = slice.match(/^\s*([^\s]+)/);
|
|
312
417
|
return match?.[1]?.length || 1;
|
|
313
418
|
}
|
|
314
|
-
|
|
419
|
+
/**
|
|
420
|
+
* `chunkOffset` places the chunk in the utterance text; `sourceOffset` maps an
|
|
421
|
+
* utterance offset to the boundary position reported for it.
|
|
422
|
+
*/
|
|
423
|
+
async speakChunk(chunkText, chunkOffset, runId, voice, sourceOffset) {
|
|
315
424
|
return new Promise((resolve, reject) => {
|
|
316
425
|
if (runId !== this.speakRunId) {
|
|
317
426
|
resolve(false);
|
|
@@ -319,7 +428,11 @@ class BrowserTTSProviderImpl {
|
|
|
319
428
|
}
|
|
320
429
|
const utterance = new SpeechSynthesisUtterance(chunkText);
|
|
321
430
|
this.utterance = utterance;
|
|
322
|
-
// Apply config
|
|
431
|
+
// Apply config. The language tells the engine how to read the text, and
|
|
432
|
+
// lets it choose a voice when the default one is left in place.
|
|
433
|
+
const contentLocale = this.getContentLocale();
|
|
434
|
+
if (contentLocale)
|
|
435
|
+
utterance.lang = contentLocale;
|
|
323
436
|
if (voice && shouldAssignBrowserVoice(voice))
|
|
324
437
|
utterance.voice = voice;
|
|
325
438
|
if (this.config?.rate)
|
|
@@ -362,7 +475,6 @@ class BrowserTTSProviderImpl {
|
|
|
362
475
|
}
|
|
363
476
|
if (runId === this.speakRunId) {
|
|
364
477
|
this._isPlaying = false;
|
|
365
|
-
this._isPaused = false;
|
|
366
478
|
}
|
|
367
479
|
if (error) {
|
|
368
480
|
reject(error);
|
|
@@ -384,7 +496,9 @@ class BrowserTTSProviderImpl {
|
|
|
384
496
|
didStart = true;
|
|
385
497
|
clearStartTimeout();
|
|
386
498
|
this._isPlaying = true;
|
|
387
|
-
this.
|
|
499
|
+
// A pause that landed while the engine was starting holds this utterance.
|
|
500
|
+
if (this._isPaused)
|
|
501
|
+
speechSynthesis.pause();
|
|
388
502
|
try {
|
|
389
503
|
this.onPlaybackStart?.();
|
|
390
504
|
}
|
|
@@ -427,27 +541,28 @@ class BrowserTTSProviderImpl {
|
|
|
427
541
|
utterance.onboundary = (event) => {
|
|
428
542
|
if (runId !== this.speakRunId)
|
|
429
543
|
return;
|
|
430
|
-
|
|
544
|
+
logger.debug(`boundary event: ${event.name}, charIndex ${event.charIndex}, charLength ${event.charLength}`);
|
|
431
545
|
if (event.name !== "word" || !this.onWordBoundary)
|
|
432
546
|
return;
|
|
433
|
-
if (this.getHighlightMode() === "sentence") {
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
547
|
const charIndex = Math.max(0, Math.min(event.charIndex, Math.max(0, chunkText.length - 1)));
|
|
437
548
|
const reportedLength = Number(event.charLength || 0);
|
|
438
549
|
const inferredLength = this.inferWordLength(chunkText, charIndex);
|
|
439
550
|
const wordLength = Number.isFinite(reportedLength) &&
|
|
440
551
|
reportedLength > 0 &&
|
|
441
|
-
reportedLength <=
|
|
552
|
+
reportedLength <= MAX_TRUSTED_REPORTED_WORD_LENGTH &&
|
|
442
553
|
charIndex + reportedLength <= chunkText.length
|
|
443
554
|
? reportedLength
|
|
444
555
|
: inferredLength;
|
|
445
556
|
const word = chunkText
|
|
446
557
|
.substring(charIndex, Math.min(chunkText.length, charIndex + wordLength))
|
|
447
558
|
.trim();
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
559
|
+
const utteranceStart = chunkOffset + charIndex;
|
|
560
|
+
const absoluteBoundaryStart = sourceOffset(utteranceStart);
|
|
561
|
+
const boundaryLength = sourceOffset(utteranceStart + wordLength - 1) +
|
|
562
|
+
1 -
|
|
563
|
+
absoluteBoundaryStart;
|
|
564
|
+
logger.debug(`word boundary "${word}" at ${absoluteBoundaryStart}`);
|
|
565
|
+
this.onWordBoundary(word, absoluteBoundaryStart, boundaryLength);
|
|
451
566
|
};
|
|
452
567
|
startTimeout = setTimeout(() => {
|
|
453
568
|
if (settled || didStart || runId !== this.speakRunId)
|
|
@@ -466,33 +581,39 @@ class BrowserTTSProviderImpl {
|
|
|
466
581
|
});
|
|
467
582
|
}
|
|
468
583
|
pause() {
|
|
469
|
-
if (this.
|
|
584
|
+
if (this._isPaused)
|
|
585
|
+
return;
|
|
586
|
+
if (this._isPlaying) {
|
|
470
587
|
speechSynthesis.pause();
|
|
588
|
+
this._isPaused = true;
|
|
589
|
+
}
|
|
590
|
+
else if (this.activeRunId === this.speakRunId) {
|
|
591
|
+
// Between utterances, or in a structural gap: the next one holds.
|
|
592
|
+
this._isPaused = true;
|
|
471
593
|
}
|
|
472
594
|
}
|
|
473
595
|
resume() {
|
|
474
|
-
if (this.
|
|
596
|
+
if (!this._isPaused)
|
|
597
|
+
return;
|
|
598
|
+
this._isPaused = false;
|
|
599
|
+
if (this._isPlaying)
|
|
475
600
|
speechSynthesis.resume();
|
|
476
|
-
|
|
601
|
+
this.releaseHold();
|
|
477
602
|
}
|
|
478
603
|
stop() {
|
|
479
604
|
const utterance = this.utterance;
|
|
480
605
|
const shouldCancel = this._isPlaying || utterance !== null;
|
|
481
606
|
this.speakRunId += 1;
|
|
482
|
-
this.
|
|
607
|
+
this.activeRunId = null;
|
|
483
608
|
this.settlePendingChunk?.();
|
|
609
|
+
this.settlePendingGap?.();
|
|
484
610
|
if (shouldCancel) {
|
|
485
611
|
speechSynthesis.cancel();
|
|
486
612
|
}
|
|
487
613
|
this.utterance = null;
|
|
488
614
|
this._isPlaying = false;
|
|
489
615
|
this._isPaused = false;
|
|
490
|
-
|
|
491
|
-
isPlaying() {
|
|
492
|
-
return this._isPlaying && !this._isPaused;
|
|
493
|
-
}
|
|
494
|
-
isPaused() {
|
|
495
|
-
return this._isPaused;
|
|
616
|
+
this.releaseHold();
|
|
496
617
|
}
|
|
497
618
|
/**
|
|
498
619
|
* Update settings dynamically (rate, pitch, voice)
|
|
@@ -502,7 +623,6 @@ class BrowserTTSProviderImpl {
|
|
|
502
623
|
if (!this.config) {
|
|
503
624
|
this.config = {};
|
|
504
625
|
}
|
|
505
|
-
// Update config with new settings
|
|
506
626
|
if (settings.rate !== undefined) {
|
|
507
627
|
this.config.rate = settings.rate;
|
|
508
628
|
}
|