@pie-players/pie-assessment-toolkit 0.3.73 → 0.3.74
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 +171 -56
- package/dist/attempt/AssessmentSession.d.ts +1 -5
- package/dist/attempt/AssessmentSession.js +0 -29
- package/dist/components/ItemToolBar.custom-element.d.ts +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +40 -0
- package/dist/components/chunks/ItemToolBar-gmrryv2v.js +2 -0
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +4 -0
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +2 -0
- package/dist/components/chunks/sre-engine-wf9s43gg.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +5 -0
- package/dist/context/runtime-context-consumer.js +5 -38
- package/dist/index.d.ts +6 -6
- package/dist/index.js +8 -9
- package/dist/policy/core/ToolPolicyEngine.d.ts +8 -2
- package/dist/policy/core/ToolPolicyEngine.js +12 -0
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +3 -3
- package/dist/runtime/adapter/SectionEngineAdapter.js +3 -3
- package/dist/runtime/registration-events.d.ts +3 -0
- package/dist/runtime/section-runtime-engine-host-context.d.ts +3 -9
- package/dist/runtime/section-runtime-engine-host-context.js +5 -43
- package/dist/runtime/tool-host-contract.d.ts +20 -0
- package/dist/runtime/tool-host-contract.js +21 -0
- package/dist/services/HighlightCoordinator.d.ts +1 -7
- package/dist/services/HighlightCoordinator.js +1 -3
- package/dist/services/TTSService.d.ts +15 -1
- package/dist/services/TTSService.js +88 -35
- package/dist/services/ToolCoordinator.d.ts +1 -7
- package/dist/services/ToolCoordinator.js +1 -3
- package/dist/services/ToolRegistry.d.ts +13 -6
- package/dist/services/ToolRegistry.js +19 -7
- package/dist/services/ToolkitCoordinator.d.ts +110 -18
- package/dist/services/ToolkitCoordinator.js +273 -85
- package/dist/services/catalog-owner.d.ts +11 -0
- package/dist/services/catalog-owner.js +25 -0
- package/dist/services/create-tools-config.d.ts +2 -2
- package/dist/services/framework-error.d.ts +8 -1
- package/dist/services/interfaces.d.ts +9 -9
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/section-controller-types.d.ts +15 -0
- package/dist/services/spoken-audio-cards.js +1 -1
- package/dist/services/tool-config-validation.d.ts +13 -2
- package/dist/services/tool-config-validation.js +33 -16
- package/dist/services/tool-context.js +29 -13
- package/dist/services/tool-providers/TTSToolProvider.d.ts +27 -65
- package/dist/services/tool-providers/TTSToolProvider.js +47 -9
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +4 -4
- package/dist/services/tool-providers/ToolProviderRegistry.js +4 -16
- package/dist/services/tool-providers/index.d.ts +0 -9
- package/dist/services/tool-providers/index.js +2 -4
- package/dist/services/tools-config-normalizer.d.ts +20 -1
- package/dist/services/tts/catalog-span-alignment.d.ts +2 -17
- package/dist/services/tts/catalog-span-alignment.js +17 -142
- package/dist/services/tts/generated-speech/types.d.ts +0 -12
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +26 -1
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
- package/dist/services/tts/math-alignment/index.d.ts +0 -5
- package/dist/services/tts/math-alignment/index.js +5 -25
- package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
- package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
- package/dist/services/tts/math-alignment/types.d.ts +4 -2
- package/dist/services/tts/math-aware-text-processing.js +44 -2
- package/dist/services/tts/math-speech.d.ts +5 -1
- package/dist/services/tts/math-speech.js +9 -6
- package/dist/services/tts/sre-engine.d.ts +7 -0
- package/dist/services/tts/sre-engine.js +11 -0
- package/dist/services/tts/sre-locales.d.ts +14 -0
- package/dist/services/tts/sre-locales.js +57 -0
- package/dist/services/tts/text-processing.d.ts +0 -1
- package/dist/services/tts/text-processing.js +0 -1
- package/dist/services/tts-runtime-config.d.ts +21 -3
- package/dist/services/tts-runtime-config.js +11 -3
- package/dist/tools/client.d.ts +1 -3
- package/dist/tools/client.js +1 -5
- package/dist/tools/internal.d.ts +6 -5
- package/dist/tools/internal.js +6 -8
- package/package.json +17 -36
- package/dist/components/chunks/ItemToolBar-byq80z42.js +0 -40
- package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
- package/dist/services/ToolConfigResolver.d.ts +0 -161
- package/dist/services/ToolConfigResolver.js +0 -229
- package/dist/services/catalog-media.d.ts +0 -56
- package/dist/services/catalog-media.js +0 -190
- package/dist/services/createDefaultToolRegistry.d.ts +0 -53
- package/dist/services/createDefaultToolRegistry.js +0 -45
- package/dist/services/tool-providers/CortexToolProvider.d.ts +0 -13
- package/dist/services/tool-providers/CortexToolProvider.js +0 -32
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
- package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
- package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
- package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
- package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
- package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
- package/dist/services/tts/visual-line-ranges.d.ts +0 -18
- package/dist/services/tts/visual-line-ranges.js +0 -226
- package/dist/tools/index.d.ts +0 -8
- package/dist/tools/index.js +0 -10
- package/dist/tools/response-discovery.d.ts +0 -61
- package/dist/tools/response-discovery.js +0 -113
- package/dist/tools/types.d.ts +0 -337
- package/dist/tools/types.js +0 -12
- package/dist/tools/variant-resolver.d.ts +0 -41
- package/dist/tools/variant-resolver.js +0 -207
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
* `scripts/check-engine-core-purity.mjs` enforce the core's purity;
|
|
40
40
|
* the adapter follows the same constraint by convention so it stays
|
|
41
41
|
* usable from non-Svelte hosts (Node tests, Storybook, or future
|
|
42
|
-
* non-Svelte consumers). `bun run check:custom-elements`
|
|
43
|
-
* blocks `.svelte` imports in published `dist`; adding a
|
|
44
|
-
* dependency here would also leak through that gate.
|
|
42
|
+
* non-Svelte consumers). `bun run check:custom-elements:dist`
|
|
43
|
+
* already blocks `.svelte` imports in published `dist`; adding a
|
|
44
|
+
* Svelte dependency here would also leak through that gate.
|
|
45
45
|
*/
|
|
46
46
|
import type { FrameworkErrorReporter } from "../../services/framework-error-bus.js";
|
|
47
47
|
import { SectionEngineCore } from "../core/SectionEngineCore.js";
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
* `scripts/check-engine-core-purity.mjs` enforce the core's purity;
|
|
40
40
|
* the adapter follows the same constraint by convention so it stays
|
|
41
41
|
* usable from non-Svelte hosts (Node tests, Storybook, or future
|
|
42
|
-
* non-Svelte consumers). `bun run check:custom-elements`
|
|
43
|
-
* blocks `.svelte` imports in published `dist`; adding a
|
|
44
|
-
* dependency here would also leak through that gate.
|
|
42
|
+
* non-Svelte consumers). `bun run check:custom-elements:dist`
|
|
43
|
+
* already blocks `.svelte` imports in published `dist`; adding a
|
|
44
|
+
* Svelte dependency here would also leak through that gate.
|
|
45
45
|
*/
|
|
46
46
|
import { SectionEngineCore } from "../core/SectionEngineCore.js";
|
|
47
47
|
import { createCoordinatorBridge, } from "./coordinator-bridge.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SessionCommitReason } from "@pie-players/pie-players-shared/pie";
|
|
1
2
|
import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
|
|
2
3
|
export declare const PIE_REGISTER_EVENT = "pie-register";
|
|
3
4
|
export declare const PIE_UNREGISTER_EVENT = "pie-unregister";
|
|
@@ -21,6 +22,8 @@ export interface ItemSessionChangedDetail {
|
|
|
21
22
|
canonicalItemId?: string;
|
|
22
23
|
session: unknown;
|
|
23
24
|
sourceRuntimeId?: string;
|
|
25
|
+
/** Set when the change is a commit at a teardown, navigation or page-hidden seam. */
|
|
26
|
+
sessionCommitReason?: SessionCommitReason;
|
|
24
27
|
}
|
|
25
28
|
export interface InternalItemSessionChangedDetail {
|
|
26
29
|
itemId: string;
|
|
@@ -62,15 +62,9 @@ export declare const sectionRuntimeEngineHostContext: {
|
|
|
62
62
|
};
|
|
63
63
|
export type SectionRuntimeEngineHostContextListener = (value: SectionRuntimeEngineHostContextValue) => void;
|
|
64
64
|
/**
|
|
65
|
-
* Connect a DOM host to the cross-CE engine context
|
|
66
|
-
* provider-retry
|
|
67
|
-
*
|
|
68
|
-
* fallback up to 200 attempts ≈ 10 s).
|
|
69
|
-
*
|
|
70
|
-
* The retry mirrors the existing
|
|
71
|
-
* `connectAssessmentToolkitHostRuntimeContext` infrastructure so the
|
|
72
|
-
* timing characteristics of cross-CE handshakes stay consistent across
|
|
73
|
-
* the toolkit's contexts.
|
|
65
|
+
* Connect a DOM host to the cross-CE engine context through
|
|
66
|
+
* `connectContextWithRetry`, the provider-retry the toolkit's other
|
|
67
|
+
* contexts use.
|
|
74
68
|
*
|
|
75
69
|
* Returns a cleanup function that disconnects the consumer and clears
|
|
76
70
|
* the retry interval. Standalone toolkits naturally exhaust the retry
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* breaking change. Adding optional fields to the value shape is
|
|
36
36
|
* allowed; required fields are not.
|
|
37
37
|
*/
|
|
38
|
-
import {
|
|
38
|
+
import { connectContextWithRetry, createContext, } from "@pie-players/pie-context";
|
|
39
39
|
/**
|
|
40
40
|
* `pie-context` context key for the cross-CE engine bridge. Uses a
|
|
41
41
|
* `Symbol.for(...)` registry key so providers and consumers in
|
|
@@ -43,15 +43,9 @@ import { ContextConsumer, createContext, } from "@pie-players/pie-context";
|
|
|
43
43
|
*/
|
|
44
44
|
export const sectionRuntimeEngineHostContext = createContext(Symbol.for("pie.sectionRuntimeEngineHostContext"));
|
|
45
45
|
/**
|
|
46
|
-
* Connect a DOM host to the cross-CE engine context
|
|
47
|
-
* provider-retry
|
|
48
|
-
*
|
|
49
|
-
* fallback up to 200 attempts ≈ 10 s).
|
|
50
|
-
*
|
|
51
|
-
* The retry mirrors the existing
|
|
52
|
-
* `connectAssessmentToolkitHostRuntimeContext` infrastructure so the
|
|
53
|
-
* timing characteristics of cross-CE handshakes stay consistent across
|
|
54
|
-
* the toolkit's contexts.
|
|
46
|
+
* Connect a DOM host to the cross-CE engine context through
|
|
47
|
+
* `connectContextWithRetry`, the provider-retry the toolkit's other
|
|
48
|
+
* contexts use.
|
|
55
49
|
*
|
|
56
50
|
* Returns a cleanup function that disconnects the consumer and clears
|
|
57
51
|
* the retry interval. Standalone toolkits naturally exhaust the retry
|
|
@@ -59,37 +53,5 @@ export const sectionRuntimeEngineHostContext = createContext(Symbol.for("pie.sec
|
|
|
59
53
|
* engine — that is by design and is not an error.
|
|
60
54
|
*/
|
|
61
55
|
export function connectSectionRuntimeEngineHostContext(host, onValue) {
|
|
62
|
-
|
|
63
|
-
const consumer = new ContextConsumer(host, {
|
|
64
|
-
context: sectionRuntimeEngineHostContext,
|
|
65
|
-
subscribe: true,
|
|
66
|
-
onValue: (value) => {
|
|
67
|
-
hasValue = true;
|
|
68
|
-
onValue(value);
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
consumer.connect();
|
|
72
|
-
const onContextProvider = (event) => {
|
|
73
|
-
if (event.context !==
|
|
74
|
-
sectionRuntimeEngineHostContext) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
consumer.requestValue();
|
|
78
|
-
};
|
|
79
|
-
host.addEventListener("context-provider", onContextProvider);
|
|
80
|
-
let attempts = 0;
|
|
81
|
-
const maxAttempts = 200;
|
|
82
|
-
const retryTimer = globalThis.setInterval(() => {
|
|
83
|
-
if (hasValue || attempts >= maxAttempts) {
|
|
84
|
-
globalThis.clearInterval(retryTimer);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
attempts += 1;
|
|
88
|
-
consumer.requestValue();
|
|
89
|
-
}, 50);
|
|
90
|
-
return () => {
|
|
91
|
-
globalThis.clearInterval(retryTimer);
|
|
92
|
-
host.removeEventListener("context-provider", onContextProvider);
|
|
93
|
-
consumer.disconnect();
|
|
94
|
-
};
|
|
56
|
+
return connectContextWithRetry(host, sectionRuntimeEngineHostContext, onValue);
|
|
95
57
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { UnknownContext } from "@pie-players/pie-context";
|
|
2
2
|
import { type RegionScopeContextListener, type RuntimeContextListener, type ShellContextListener } from "../context/runtime-context-consumer.js";
|
|
3
|
+
import type { ZIndexLayer } from "../services/ToolCoordinator.js";
|
|
4
|
+
import type { ToolCoordinatorApi } from "../services/interfaces.js";
|
|
3
5
|
type BaseEventInit = Pick<EventInit, "bubbles" | "composed" | "cancelable">;
|
|
4
6
|
/**
|
|
5
7
|
* Contract helper for all toolkit/tool events that must cross custom-element boundaries.
|
|
@@ -22,6 +24,24 @@ export declare function connectToolShellContext(host: HTMLElement, onValue: Shel
|
|
|
22
24
|
* Shared region-scope context contract for tools targeting content subregions.
|
|
23
25
|
*/
|
|
24
26
|
export declare function connectToolRegionScopeContext(host: HTMLElement, onValue: RegionScopeContextListener): () => void;
|
|
27
|
+
/** A tool's registration with the coordinator in its runtime context. */
|
|
28
|
+
export interface ToolCoordinatorRegistration {
|
|
29
|
+
/**
|
|
30
|
+
* Registers `toolId` with `coordinator`, first unregistering it from the
|
|
31
|
+
* coordinator or id it was registered under. The coordinator arrives through
|
|
32
|
+
* a republished runtime context, so a new instance can replace the old one
|
|
33
|
+
* mid-session; a one-shot registration would leave z-index, `bringToFront`
|
|
34
|
+
* and visibility-restore bound to the dead one. Call it from an effect on
|
|
35
|
+
* both; it does nothing until both are set, or when neither changed.
|
|
36
|
+
*/
|
|
37
|
+
sync(coordinator: ToolCoordinatorApi | null | undefined, toolId: string | null | undefined): void;
|
|
38
|
+
/**
|
|
39
|
+
* Unregisters from the coordinator the registration was made against, which
|
|
40
|
+
* is not necessarily the one currently in context.
|
|
41
|
+
*/
|
|
42
|
+
release(): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function createToolCoordinatorRegistration(name: string, layer: ZIndexLayer): ToolCoordinatorRegistration;
|
|
25
45
|
/**
|
|
26
46
|
* Guard utility: narrows unknown context payloads when needed by callers.
|
|
27
47
|
*/
|
|
@@ -39,6 +39,27 @@ export function connectToolShellContext(host, onValue) {
|
|
|
39
39
|
export function connectToolRegionScopeContext(host, onValue) {
|
|
40
40
|
return connectAssessmentToolkitRegionScopeContext(host, onValue);
|
|
41
41
|
}
|
|
42
|
+
export function createToolCoordinatorRegistration(name, layer) {
|
|
43
|
+
let registered = null;
|
|
44
|
+
function release() {
|
|
45
|
+
registered?.coordinator.unregisterTool(registered.toolId);
|
|
46
|
+
registered = null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
sync(coordinator, toolId) {
|
|
50
|
+
if (!coordinator || !toolId)
|
|
51
|
+
return;
|
|
52
|
+
if (registered?.coordinator === coordinator &&
|
|
53
|
+
registered.toolId === toolId) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
release();
|
|
57
|
+
coordinator.registerTool(toolId, name, undefined, layer);
|
|
58
|
+
registered = { coordinator, toolId };
|
|
59
|
+
},
|
|
60
|
+
release,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
42
63
|
/**
|
|
43
64
|
* Guard utility: narrows unknown context payloads when needed by callers.
|
|
44
65
|
*/
|
|
@@ -51,13 +51,7 @@ export interface Annotation {
|
|
|
51
51
|
color: HighlightColor;
|
|
52
52
|
timestamp: number;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Configuration for HighlightCoordinator
|
|
56
|
-
* Currently empty but allows future extension without breaking changes
|
|
57
|
-
*/
|
|
58
|
-
export type HighlightCoordinatorConfig = Record<string, never>;
|
|
59
54
|
export declare class HighlightCoordinator implements HighlightCoordinatorApi {
|
|
60
|
-
private config;
|
|
61
55
|
private ttsWordHighlight;
|
|
62
56
|
private ttsSentenceHighlight;
|
|
63
57
|
private ttsWordElementHighlights;
|
|
@@ -69,7 +63,7 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
|
|
|
69
63
|
private rangeSerializer;
|
|
70
64
|
private themeObserver;
|
|
71
65
|
private explicitTTSColorOverride;
|
|
72
|
-
constructor(
|
|
66
|
+
constructor();
|
|
73
67
|
private setupThemeObservation;
|
|
74
68
|
private parseColor;
|
|
75
69
|
private relativeLuminance;
|
|
@@ -43,7 +43,6 @@ export var HighlightColor;
|
|
|
43
43
|
HighlightColor["UNDERLINE"] = "underline";
|
|
44
44
|
})(HighlightColor || (HighlightColor = {}));
|
|
45
45
|
export class HighlightCoordinator {
|
|
46
|
-
config;
|
|
47
46
|
ttsWordHighlight = null;
|
|
48
47
|
ttsSentenceHighlight = null;
|
|
49
48
|
ttsWordElementHighlights = new Set();
|
|
@@ -56,8 +55,7 @@ export class HighlightCoordinator {
|
|
|
56
55
|
rangeSerializer;
|
|
57
56
|
themeObserver = null;
|
|
58
57
|
explicitTTSColorOverride = null;
|
|
59
|
-
constructor(
|
|
60
|
-
this.config = config;
|
|
58
|
+
constructor() {
|
|
61
59
|
this.rangeSerializer = new RangeSerializer();
|
|
62
60
|
// SSR guard
|
|
63
61
|
if (typeof CSS === "undefined" || !("highlights" in CSS)) {
|
|
@@ -66,7 +66,6 @@ export declare class TTSService {
|
|
|
66
66
|
private playbackChunks;
|
|
67
67
|
private activeRecordedAudio;
|
|
68
68
|
private sentenceHighlightSegments;
|
|
69
|
-
private currentSeekSegmentIndex;
|
|
70
69
|
private activeSentenceStartOffset;
|
|
71
70
|
private playbackStartDeferredRunId;
|
|
72
71
|
private pendingPlaybackStartHighlights;
|
|
@@ -171,6 +170,11 @@ export declare class TTSService {
|
|
|
171
170
|
private isElementHidden;
|
|
172
171
|
private getBoundaryStrength;
|
|
173
172
|
private getBoundaryAnchor;
|
|
173
|
+
/**
|
|
174
|
+
* `textOffset` is where `normalizedText` starts in the element's visible text:
|
|
175
|
+
* boundaries are collected over the whole element, and a selection speaks only
|
|
176
|
+
* part of it.
|
|
177
|
+
*/
|
|
174
178
|
private createSpeechPlan;
|
|
175
179
|
private collectSpeechPlanBoundaries;
|
|
176
180
|
private createNodeStartOffsets;
|
|
@@ -182,6 +186,11 @@ export declare class TTSService {
|
|
|
182
186
|
private createSeekSegmentsFromSpeechChunks;
|
|
183
187
|
private getCurrentSeekSegmentIndex;
|
|
184
188
|
private speakWithPlan;
|
|
189
|
+
/**
|
|
190
|
+
* `startOffset` indexes the spoken text; the position map indexes the whole
|
|
191
|
+
* content element, which for a selection starts `activeWordBoundaryOffset`
|
|
192
|
+
* characters earlier.
|
|
193
|
+
*/
|
|
185
194
|
private highlightSentenceSegment;
|
|
186
195
|
private getSegmentIndexForOffset;
|
|
187
196
|
private highlightSentenceForOffset;
|
|
@@ -252,6 +261,11 @@ export declare class TTSService {
|
|
|
252
261
|
*
|
|
253
262
|
* @param range DOM Range to speak
|
|
254
263
|
*/
|
|
264
|
+
/**
|
|
265
|
+
* The selected part of `root`'s normalized visible text and where it starts, or
|
|
266
|
+
* null when the map is unavailable or holds no selected character.
|
|
267
|
+
*/
|
|
268
|
+
private selectMappedRangeText;
|
|
255
269
|
speakRange(range: Range, options?: {
|
|
256
270
|
contentRoot?: Element | null;
|
|
257
271
|
}): Promise<void>;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Part of PIE Assessment Toolkit.
|
|
17
17
|
*/
|
|
18
|
-
import { applyMediaFragment, enforceMediaFragment } from "
|
|
18
|
+
import { applyMediaFragment, enforceMediaFragment, } from "@pie-players/pie-players-shared/media";
|
|
19
19
|
import { HighlightColor, HighlightType } from "./HighlightCoordinator.js";
|
|
20
20
|
import { resolveSpokenAudioMedia, } from "./spoken-audio-cards.js";
|
|
21
21
|
import { BrowserTTSProvider } from "./tts/browser-provider.js";
|
|
@@ -95,7 +95,6 @@ export class TTSService {
|
|
|
95
95
|
// chunk loop on a run that has already been superseded.
|
|
96
96
|
activeRecordedAudio = null;
|
|
97
97
|
sentenceHighlightSegments = [];
|
|
98
|
-
currentSeekSegmentIndex = 0;
|
|
99
98
|
activeSentenceStartOffset = null;
|
|
100
99
|
playbackStartDeferredRunId = null;
|
|
101
100
|
pendingPlaybackStartHighlights = [];
|
|
@@ -675,9 +674,21 @@ export class TTSService {
|
|
|
675
674
|
units: this.getStructuralPauseProfile().units[strength],
|
|
676
675
|
};
|
|
677
676
|
}
|
|
678
|
-
|
|
677
|
+
/**
|
|
678
|
+
* `textOffset` is where `normalizedText` starts in the element's visible text:
|
|
679
|
+
* boundaries are collected over the whole element, and a selection speaks only
|
|
680
|
+
* part of it.
|
|
681
|
+
*/
|
|
682
|
+
createSpeechPlan(contentElement, normalizedText, textOffset = 0) {
|
|
679
683
|
const boundaries = this.collectSpeechPlanBoundaries(contentElement, normalizedText);
|
|
680
|
-
|
|
684
|
+
if (textOffset === 0) {
|
|
685
|
+
return this.createSpeechPlanSegments(normalizedText, boundaries);
|
|
686
|
+
}
|
|
687
|
+
const shifted = new Map();
|
|
688
|
+
for (const [point, units] of boundaries) {
|
|
689
|
+
shifted.set(point - textOffset, units);
|
|
690
|
+
}
|
|
691
|
+
return this.createSpeechPlanSegments(normalizedText, shifted);
|
|
681
692
|
}
|
|
682
693
|
collectSpeechPlanBoundaries(contentElement, normalizedText) {
|
|
683
694
|
const boundaries = new Map();
|
|
@@ -858,7 +869,6 @@ export class TTSService {
|
|
|
858
869
|
originalOnWordBoundary?.(word, position, length);
|
|
859
870
|
if (Number.isFinite(position)) {
|
|
860
871
|
this.currentBoundaryOffset = position;
|
|
861
|
-
this.currentSeekSegmentIndex = this.getCurrentSeekSegmentIndex();
|
|
862
872
|
}
|
|
863
873
|
};
|
|
864
874
|
try {
|
|
@@ -878,10 +888,6 @@ export class TTSService {
|
|
|
878
888
|
for (const segment of segments) {
|
|
879
889
|
if (runId !== this.speakRunId)
|
|
880
890
|
return;
|
|
881
|
-
const seekIndex = this.seekSegments.findIndex((candidate) => candidate.startOffset === segment.startOffset);
|
|
882
|
-
if (seekIndex >= 0) {
|
|
883
|
-
this.currentSeekSegmentIndex = seekIndex;
|
|
884
|
-
}
|
|
885
891
|
this.currentBoundaryOffset = segment.startOffset;
|
|
886
892
|
if (shouldTrackSentenceProgress) {
|
|
887
893
|
this.runWhenPlaybackStarts(runId, () => {
|
|
@@ -895,14 +901,20 @@ export class TTSService {
|
|
|
895
901
|
}
|
|
896
902
|
}
|
|
897
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* `startOffset` indexes the spoken text; the position map indexes the whole
|
|
906
|
+
* content element, which for a selection starts `activeWordBoundaryOffset`
|
|
907
|
+
* characters earlier.
|
|
908
|
+
*/
|
|
898
909
|
highlightSentenceSegment(startOffset, text) {
|
|
899
910
|
if (!this.highlightCoordinator || typeof document === "undefined")
|
|
900
911
|
return;
|
|
901
912
|
const length = text.trimEnd().length;
|
|
902
913
|
if (length <= 0)
|
|
903
914
|
return;
|
|
904
|
-
const
|
|
905
|
-
const
|
|
915
|
+
const mapStart = startOffset + this.activeWordBoundaryOffset;
|
|
916
|
+
const start = this.normalizedToDOM.get(mapStart);
|
|
917
|
+
const end = this.normalizedToDOM.get(mapStart + length - 1);
|
|
906
918
|
if (!start || !end)
|
|
907
919
|
return;
|
|
908
920
|
const range = document.createRange();
|
|
@@ -1030,7 +1042,7 @@ export class TTSService {
|
|
|
1030
1042
|
: hasExplicitBreaks || !speechMatchesVisibleText
|
|
1031
1043
|
? []
|
|
1032
1044
|
: shouldUsePlan && this.currentContentElement
|
|
1033
|
-
? this.createSpeechPlan(this.currentContentElement, normalizedText)
|
|
1045
|
+
? this.createSpeechPlan(this.currentContentElement, normalizedText, options?.wordBoundaryOffset || 0)
|
|
1034
1046
|
: this.createSeekSegmentsFromText(highlightText);
|
|
1035
1047
|
this.sentenceHighlightSegments = hasExplicitBreaks
|
|
1036
1048
|
? []
|
|
@@ -1520,7 +1532,6 @@ export class TTSService {
|
|
|
1520
1532
|
this.lastError = null;
|
|
1521
1533
|
this.currentBoundaryOffset = 0;
|
|
1522
1534
|
this.playbackChunks = [];
|
|
1523
|
-
this.currentSeekSegmentIndex = 0;
|
|
1524
1535
|
this.activeSentenceStartOffset = null;
|
|
1525
1536
|
}
|
|
1526
1537
|
prepareHighlightsForSpeak(args) {
|
|
@@ -1559,8 +1570,9 @@ export class TTSService {
|
|
|
1559
1570
|
// or ServerTTSProvider time-scaled onWordBoundary).
|
|
1560
1571
|
this.provider.onWordBoundary = (word, charIndex, length) => {
|
|
1561
1572
|
const wordLength = length || word.length;
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1573
|
+
const spokenIndex = charIndex + this.currentBoundaryOffset;
|
|
1574
|
+
const globalIndex = spokenIndex + args.wordBoundaryOffset;
|
|
1575
|
+
this.highlightSentenceForOffset(spokenIndex);
|
|
1564
1576
|
const highlightRange = this.findHighlightRange(globalIndex, wordLength);
|
|
1565
1577
|
if (highlightRange && this.highlightCoordinator) {
|
|
1566
1578
|
const highlightText = highlightRange.node.textContent?.substring(highlightRange.start, highlightRange.end) || "";
|
|
@@ -1907,7 +1919,6 @@ export class TTSService {
|
|
|
1907
1919
|
for (let index = 0; index < args.speechChunks.length; index++) {
|
|
1908
1920
|
if (args.runId !== this.speakRunId)
|
|
1909
1921
|
return;
|
|
1910
|
-
this.currentSeekSegmentIndex = index;
|
|
1911
1922
|
const segment = this.seekSegments[index];
|
|
1912
1923
|
if (segment) {
|
|
1913
1924
|
this.currentBoundaryOffset = segment.startOffset;
|
|
@@ -1947,7 +1958,6 @@ export class TTSService {
|
|
|
1947
1958
|
this.seekSegments = [];
|
|
1948
1959
|
this.playbackChunks = [];
|
|
1949
1960
|
this.sentenceHighlightSegments = [];
|
|
1950
|
-
this.currentSeekSegmentIndex = 0;
|
|
1951
1961
|
this.activeSentenceStartOffset = null;
|
|
1952
1962
|
}
|
|
1953
1963
|
/**
|
|
@@ -1958,6 +1968,42 @@ export class TTSService {
|
|
|
1958
1968
|
*
|
|
1959
1969
|
* @param range DOM Range to speak
|
|
1960
1970
|
*/
|
|
1971
|
+
/**
|
|
1972
|
+
* The selected part of `root`'s normalized visible text and where it starts, or
|
|
1973
|
+
* null when the map is unavailable or holds no selected character.
|
|
1974
|
+
*/
|
|
1975
|
+
selectMappedRangeText(range, root) {
|
|
1976
|
+
if (typeof range.comparePoint !== "function")
|
|
1977
|
+
return null;
|
|
1978
|
+
const { text: rootText, map } = collectVisibleTextAndMap(root, this.getTextProcessingOptions());
|
|
1979
|
+
let start = -1;
|
|
1980
|
+
let end = -1;
|
|
1981
|
+
for (const [index, { node, offset }] of map) {
|
|
1982
|
+
if (index >= rootText.length)
|
|
1983
|
+
continue;
|
|
1984
|
+
let inside = false;
|
|
1985
|
+
try {
|
|
1986
|
+
inside =
|
|
1987
|
+
range.comparePoint(node, offset) === 0 &&
|
|
1988
|
+
range.comparePoint(node, Math.min(offset + 1, node.length)) === 0;
|
|
1989
|
+
}
|
|
1990
|
+
catch {
|
|
1991
|
+
inside = false;
|
|
1992
|
+
}
|
|
1993
|
+
if (!inside)
|
|
1994
|
+
continue;
|
|
1995
|
+
if (start === -1 || index < start)
|
|
1996
|
+
start = index;
|
|
1997
|
+
if (index > end)
|
|
1998
|
+
end = index;
|
|
1999
|
+
}
|
|
2000
|
+
if (start === -1)
|
|
2001
|
+
return null;
|
|
2002
|
+
const raw = rootText.slice(start, end + 1);
|
|
2003
|
+
const leading = raw.length - raw.trimStart().length;
|
|
2004
|
+
const text = raw.trim();
|
|
2005
|
+
return text ? { text, offset: start + leading } : null;
|
|
2006
|
+
}
|
|
1961
2007
|
async speakRange(range, options) {
|
|
1962
2008
|
if (!this.provider) {
|
|
1963
2009
|
throw new Error("TTS service not initialized");
|
|
@@ -1983,28 +2029,38 @@ export class TTSService {
|
|
|
1983
2029
|
if (!root)
|
|
1984
2030
|
return;
|
|
1985
2031
|
const selected = collectRangeTextForSpeech(range, root);
|
|
1986
|
-
const
|
|
1987
|
-
if (!
|
|
2032
|
+
const selectedText = selected.text.trim();
|
|
2033
|
+
if (!selectedText) {
|
|
1988
2034
|
if (selected.filtered) {
|
|
1989
2035
|
console.warn("[tts] every part of the selection is either hidden or marked not-to-be-spoken; nothing was spoken.");
|
|
1990
2036
|
}
|
|
1991
2037
|
return;
|
|
1992
2038
|
}
|
|
1993
|
-
//
|
|
1994
|
-
// the
|
|
1995
|
-
// text,
|
|
1996
|
-
//
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2039
|
+
// The spoken text and its offset come from the root's visible-text map, which
|
|
2040
|
+
// the highlight position map and the structural speech plan index too. Joined
|
|
2041
|
+
// from raw text nodes instead, a selection loses the space the map inserts
|
|
2042
|
+
// between blocks, and every offset after that boundary drifts.
|
|
2043
|
+
const mapped = this.selectMappedRangeText(range, root);
|
|
2044
|
+
let text = selectedText;
|
|
2045
|
+
let offset = mapped?.offset ?? 0;
|
|
2046
|
+
if (mapped) {
|
|
2047
|
+
text = mapped.text;
|
|
2048
|
+
}
|
|
2049
|
+
else {
|
|
2050
|
+
// Filtered the same way as the speech itself: the offset indexes into the
|
|
2051
|
+
// highlight text, which comes from the exclusion-aware collectors.
|
|
2052
|
+
const beforeRange = document.createRange();
|
|
2053
|
+
beforeRange.selectNodeContents(root);
|
|
2054
|
+
beforeRange.setEnd(range.startContainer, range.startOffset);
|
|
2055
|
+
const textBeforeRange = collectRangeTextForSpeech(beforeRange, root).text;
|
|
2056
|
+
const normalizedTextBeforeRange = normalizeTextForSpeech(textBeforeRange);
|
|
2057
|
+
offset =
|
|
2058
|
+
normalizedTextBeforeRange.length +
|
|
2059
|
+
(/\s$/.test(textBeforeRange) && normalizedTextBeforeRange ? 1 : 0);
|
|
2060
|
+
}
|
|
2005
2061
|
this.debugLog("[TTSService] speakRange offset calculation:", {
|
|
2006
2062
|
selectedText: text,
|
|
2007
|
-
|
|
2063
|
+
mapped: !!mapped,
|
|
2008
2064
|
offset,
|
|
2009
2065
|
rootTag: root.tagName,
|
|
2010
2066
|
});
|
|
@@ -2058,7 +2114,6 @@ export class TTSService {
|
|
|
2058
2114
|
this.cancelRecordedAudio();
|
|
2059
2115
|
this.provider.onWordBoundary = undefined;
|
|
2060
2116
|
this.provider.stop();
|
|
2061
|
-
this.currentSeekSegmentIndex = safeTargetIndex;
|
|
2062
2117
|
const runId = ++this.speakRunId;
|
|
2063
2118
|
const restartSegments = this.seekSegments.slice(safeTargetIndex);
|
|
2064
2119
|
this.highlightCoordinator?.clearTTS();
|
|
@@ -2079,7 +2134,6 @@ export class TTSService {
|
|
|
2079
2134
|
if (runId !== this.speakRunId)
|
|
2080
2135
|
return;
|
|
2081
2136
|
const absoluteIndex = safeTargetIndex + index;
|
|
2082
|
-
this.currentSeekSegmentIndex = absoluteIndex;
|
|
2083
2137
|
const segment = this.seekSegments[absoluteIndex];
|
|
2084
2138
|
if (segment) {
|
|
2085
2139
|
this.currentBoundaryOffset = segment.startOffset;
|
|
@@ -2222,7 +2276,6 @@ export class TTSService {
|
|
|
2222
2276
|
this.seekSegments = [];
|
|
2223
2277
|
this.playbackChunks = [];
|
|
2224
2278
|
this.sentenceHighlightSegments = [];
|
|
2225
|
-
this.currentSeekSegmentIndex = 0;
|
|
2226
2279
|
this.activeSentenceStartOffset = null;
|
|
2227
2280
|
this.activePlaybackRate = null;
|
|
2228
2281
|
}
|
|
@@ -23,13 +23,7 @@ export declare enum ZIndexLayer {
|
|
|
23
23
|
CONTROL = 3000,// Drag handles, resize controls (3000-3999)
|
|
24
24
|
HIGHLIGHT = 4000
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Configuration for ToolCoordinator
|
|
28
|
-
* Currently empty but allows future extension without breaking changes
|
|
29
|
-
*/
|
|
30
|
-
export type ToolCoordinatorConfig = Record<string, never>;
|
|
31
26
|
export declare class ToolCoordinator implements ToolCoordinatorApi {
|
|
32
|
-
private config;
|
|
33
27
|
private tools;
|
|
34
28
|
private layerCounters;
|
|
35
29
|
private listeners;
|
|
@@ -42,7 +36,7 @@ export declare class ToolCoordinator implements ToolCoordinatorApi {
|
|
|
42
36
|
* Cleared on genuine teardown via {@link releaseTool}.
|
|
43
37
|
*/
|
|
44
38
|
private visibilityState;
|
|
45
|
-
constructor(
|
|
39
|
+
constructor();
|
|
46
40
|
/**
|
|
47
41
|
* Subscribe to tool state changes
|
|
48
42
|
* Returns unsubscribe function
|
|
@@ -26,7 +26,6 @@ export var ZIndexLayer;
|
|
|
26
26
|
ZIndexLayer[ZIndexLayer["HIGHLIGHT"] = 4000] = "HIGHLIGHT";
|
|
27
27
|
})(ZIndexLayer || (ZIndexLayer = {}));
|
|
28
28
|
export class ToolCoordinator {
|
|
29
|
-
config;
|
|
30
29
|
tools = new Map();
|
|
31
30
|
layerCounters = new Map();
|
|
32
31
|
listeners = new Set();
|
|
@@ -39,8 +38,7 @@ export class ToolCoordinator {
|
|
|
39
38
|
* Cleared on genuine teardown via {@link releaseTool}.
|
|
40
39
|
*/
|
|
41
40
|
visibilityState = new Map();
|
|
42
|
-
constructor(
|
|
43
|
-
this.config = config;
|
|
41
|
+
constructor() {
|
|
44
42
|
// Initialize layer counters
|
|
45
43
|
this.layerCounters.set(ZIndexLayer.BASE, 0);
|
|
46
44
|
this.layerCounters.set(ZIndexLayer.TOOL, 0);
|
|
@@ -24,8 +24,8 @@ export interface ToolToolbarButtonDefinition {
|
|
|
24
24
|
toolId: string;
|
|
25
25
|
label: string;
|
|
26
26
|
/**
|
|
27
|
-
* Optional to match what the
|
|
28
|
-
*
|
|
27
|
+
* Optional to match what the renderer already does: `ItemToolBar.svelte`
|
|
28
|
+
* guards on `button.icon`, and `ToolbarItem.icon` is
|
|
29
29
|
* already optional, so requiring it here claimed a guarantee nothing relied
|
|
30
30
|
* on. A registration that renders a button still has to declare an icon —
|
|
31
31
|
* `assertToolRegistrationShape` enforces that.
|
|
@@ -216,6 +216,15 @@ export interface ToolProviderDescriptor {
|
|
|
216
216
|
getAuthFetcher?: (config: ToolRuntimeConfig | undefined) => (() => Promise<Record<string, unknown>>) | undefined;
|
|
217
217
|
lazy?: boolean;
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* The id a registration's provider registers under for one tool config: the
|
|
221
|
+
* descriptor's answer, then the config's `provider.id`, then the tool id. `null`
|
|
222
|
+
* when the registration carries no provider.
|
|
223
|
+
*
|
|
224
|
+
* The coordinator registers providers under this id and the section player looks
|
|
225
|
+
* for them in a host-supplied coordinator by it, so the two resolve it one way.
|
|
226
|
+
*/
|
|
227
|
+
export declare function resolveToolProviderId(registration: Pick<ToolRegistration, "toolId" | "provider">, config: ToolRuntimeConfig | undefined): string | null;
|
|
219
228
|
export interface ToolToolbarRenderResult {
|
|
220
229
|
toolId: string;
|
|
221
230
|
elements?: ToolRenderElement[];
|
|
@@ -230,12 +239,10 @@ export interface ToolToolbarRenderResult {
|
|
|
230
239
|
* falls back to `name` rather than rendering the key, so a catalog gap degrades
|
|
231
240
|
* to English instead of to `tools.something.name` on a toolbar button.
|
|
232
241
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
242
|
+
* It lives here so the toolbars, the settings panels and the PNP debugger
|
|
243
|
+
* cannot each invent their own precedence.
|
|
235
244
|
*/
|
|
236
245
|
export declare function resolveToolRegistrationName(registration: Pick<ToolRegistration, "name" | "nameKey">, i18n?: I18nProvider): string;
|
|
237
|
-
/** A registration's description in the interface locale. See the name resolver. */
|
|
238
|
-
export declare function resolveToolRegistrationDescription(registration: Pick<ToolRegistration, "description" | "descriptionKey">, i18n?: I18nProvider): string;
|
|
239
246
|
export type ToolActivation = "toolbar-toggle" | "selection-gateway" | "region";
|
|
240
247
|
export type ToolSingletonScope = "section";
|
|
241
248
|
/**
|
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { dynamicMessageKey } from "@pie-players/pie-players-shared/i18n/provider";
|
|
8
8
|
import { normalizeToolAlias } from "./tools-config-normalizer.js";
|
|
9
|
+
/**
|
|
10
|
+
* The id a registration's provider registers under for one tool config: the
|
|
11
|
+
* descriptor's answer, then the config's `provider.id`, then the tool id. `null`
|
|
12
|
+
* when the registration carries no provider.
|
|
13
|
+
*
|
|
14
|
+
* The coordinator registers providers under this id and the section player looks
|
|
15
|
+
* for them in a host-supplied coordinator by it, so the two resolve it one way.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveToolProviderId(registration, config) {
|
|
18
|
+
const descriptor = registration.provider;
|
|
19
|
+
if (!descriptor)
|
|
20
|
+
return null;
|
|
21
|
+
return (descriptor.getProviderId?.(config) ??
|
|
22
|
+
config?.provider?.id ??
|
|
23
|
+
registration.toolId);
|
|
24
|
+
}
|
|
9
25
|
/**
|
|
10
26
|
* A registration's display name in the interface locale.
|
|
11
27
|
*
|
|
@@ -13,16 +29,12 @@ import { normalizeToolAlias } from "./tools-config-normalizer.js";
|
|
|
13
29
|
* falls back to `name` rather than rendering the key, so a catalog gap degrades
|
|
14
30
|
* to English instead of to `tools.something.name` on a toolbar button.
|
|
15
31
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
32
|
+
* It lives here so the toolbars, the settings panels and the PNP debugger
|
|
33
|
+
* cannot each invent their own precedence.
|
|
18
34
|
*/
|
|
19
35
|
export function resolveToolRegistrationName(registration, i18n) {
|
|
20
36
|
return resolveKeyedString(registration.name, registration.nameKey, i18n);
|
|
21
37
|
}
|
|
22
|
-
/** A registration's description in the interface locale. See the name resolver. */
|
|
23
|
-
export function resolveToolRegistrationDescription(registration, i18n) {
|
|
24
|
-
return resolveKeyedString(registration.description, registration.descriptionKey, i18n);
|
|
25
|
-
}
|
|
26
38
|
function resolveKeyedString(source, key, i18n) {
|
|
27
39
|
if (!key || !i18n)
|
|
28
40
|
return source;
|
|
@@ -50,7 +62,7 @@ function assertNonEmptyString(value, fieldName) {
|
|
|
50
62
|
}
|
|
51
63
|
// Defence-in-depth: reject obvious XSS payloads in tool-registered icon
|
|
52
64
|
// markup at registration time. Runtime rendering still runs each icon
|
|
53
|
-
// through DOMPurify (
|
|
65
|
+
// through DOMPurify (`sanitizeSvgIcon` in `ItemToolBar.svelte`), but surfacing the problem
|
|
54
66
|
// early produces a clearer error for tool authors than "the icon silently
|
|
55
67
|
// disappeared after sanitization".
|
|
56
68
|
const SCRIPTABLE_ICON_PATTERNS = [
|