@pie-players/pie-assessment-toolkit 0.3.47 → 0.3.49

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 (86) hide show
  1. package/dist/attempt/AssessmentSession.js +2 -1
  2. package/dist/attempt/AssessmentSession.js.map +1 -1
  3. package/dist/attempt/adapters/activity-to-test-attempt-session.js +12 -8
  4. package/dist/attempt/adapters/activity-to-test-attempt-session.js.map +1 -1
  5. package/dist/components/ItemToolBar.custom-element.js +56 -17
  6. package/dist/components/PieAssessmentToolkit.custom-element.js +84 -29
  7. package/dist/components/SectionToolBar.custom-element.js +55 -17
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/policy/core/pnp-policy-inputs.js +1 -2
  11. package/dist/policy/core/pnp-policy-inputs.js.map +1 -1
  12. package/dist/policy/core/provenance.js.map +1 -1
  13. package/dist/runtime/SectionRuntimeEngine.js +1 -1
  14. package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
  15. package/dist/runtime/core/cohort.js.map +1 -1
  16. package/dist/runtime/core/engine-input.js.map +1 -1
  17. package/dist/runtime/core/engine-readiness.js.map +1 -1
  18. package/dist/runtime/core/engine-resolver.js.map +1 -1
  19. package/dist/runtime/core/engine-transition.js +1 -3
  20. package/dist/runtime/core/engine-transition.js.map +1 -1
  21. package/dist/runtime/registration-events.js.map +1 -1
  22. package/dist/runtime/session-event-emitter-policy.js.map +1 -1
  23. package/dist/services/HighlightCoordinator.js +2 -1
  24. package/dist/services/HighlightCoordinator.js.map +1 -1
  25. package/dist/services/I18nService.d.ts +1 -1
  26. package/dist/services/I18nService.js.map +1 -1
  27. package/dist/services/SSMLExtractor.js.map +1 -1
  28. package/dist/services/TTSService.js +5 -2
  29. package/dist/services/TTSService.js.map +1 -1
  30. package/dist/services/ThemeProvider.js.map +1 -1
  31. package/dist/services/ToolRegistry.d.ts +1 -1
  32. package/dist/services/ToolRegistry.js.map +1 -1
  33. package/dist/services/ToolkitCoordinator.js +21 -11
  34. package/dist/services/ToolkitCoordinator.js.map +1 -1
  35. package/dist/services/createDefaultToolRegistry.js +4 -2
  36. package/dist/services/createDefaultToolRegistry.js.map +1 -1
  37. package/dist/services/framework-error-bus.js.map +1 -1
  38. package/dist/services/framework-error.js.map +1 -1
  39. package/dist/services/interfaces.js.map +1 -1
  40. package/dist/services/section-controller-types.js.map +1 -1
  41. package/dist/services/tool-config-defaults.js +7 -1
  42. package/dist/services/tool-config-defaults.js.map +1 -1
  43. package/dist/services/tool-config-validation.js +3 -1
  44. package/dist/services/tool-config-validation.js.map +1 -1
  45. package/dist/services/tool-instance-id.js.map +1 -1
  46. package/dist/services/tool-providers/DesmosToolProvider.js.map +1 -1
  47. package/dist/services/tool-providers/TTSToolProvider.js.map +1 -1
  48. package/dist/services/toolbar-items.js.map +1 -1
  49. package/dist/services/tools-config-normalizer.js +1 -2
  50. package/dist/services/tools-config-normalizer.js.map +1 -1
  51. package/dist/services/tts/browser-provider.js +60 -18
  52. package/dist/services/tts/browser-provider.js.map +1 -1
  53. package/dist/services/tts/catalog-span-alignment.js +8 -3
  54. package/dist/services/tts/catalog-span-alignment.js.map +1 -1
  55. package/dist/services/tts/generated-speech/assemble-plan.js.map +1 -1
  56. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  57. package/dist/services/tts/generated-speech/index.js +1 -1
  58. package/dist/services/tts/generated-speech/index.js.map +1 -1
  59. package/dist/services/tts/generated-speech/math-speech-cache.js +3 -1
  60. package/dist/services/tts/generated-speech/math-speech-cache.js.map +1 -1
  61. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js.map +1 -1
  62. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js.map +1 -1
  63. package/dist/services/tts/math-alignment/mathml-tokenizer.js.map +1 -1
  64. package/dist/services/tts/math-alignment/sequence-aligner.js.map +1 -1
  65. package/dist/services/tts/math-alignment/speech-tokenizer.js.map +1 -1
  66. package/dist/services/tts/math-aware-text-processing.js +6 -3
  67. package/dist/services/tts/math-aware-text-processing.js.map +1 -1
  68. package/dist/services/tts/math-speech.js +3 -1
  69. package/dist/services/tts/math-speech.js.map +1 -1
  70. package/dist/services/tts/visual-line-ranges.js.map +1 -1
  71. package/dist/services/tts-runtime-config.js +6 -2
  72. package/dist/services/tts-runtime-config.js.map +1 -1
  73. package/dist/tools/calculators/desmos-provider.js.map +1 -1
  74. package/dist/tools/default-tool-module-loaders.js.map +1 -1
  75. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  76. package/dist/tools/registrations/calculator.js +1 -1
  77. package/dist/tools/registrations/calculator.js.map +1 -1
  78. package/dist/tools/registrations/interaction-tools.js.map +1 -1
  79. package/dist/tools/registrations/measurement-tools.js.map +1 -1
  80. package/dist/tools/registrations/subject-specific-tools.js +1 -1
  81. package/dist/tools/registrations/subject-specific-tools.js.map +1 -1
  82. package/dist/tools/registrations/tts.js +4 -2
  83. package/dist/tools/registrations/tts.js.map +1 -1
  84. package/dist/tools/types.js.map +1 -1
  85. package/dist/tools/variant-resolver.js.map +1 -1
  86. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"TTSToolProvider.js","sourceRoot":"","sources":["../../../src/services/tool-providers/TTSToolProvider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TTS Tool Provider\n *\n * Unified provider for all TTS backends:\n * - Browser Web Speech API (no auth)\n * - AWS Polly (requires auth via server proxy)\n * - Google Cloud TTS (requires auth via server proxy)\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type { ITTSProvider, TTSConfig } from \"@pie-players/pie-tts\";\nimport { BrowserTTSProvider } from \"../../services/tts/browser-provider.js\";\nimport type { ToolProviderApi, ToolProviderCapabilities } from \"./ToolProviderApi.js\";\n\n/**\n * TTS backend type\n */\nexport type TTSBackend =\n\t| \"browser\"\n\t| \"polly\"\n\t| \"google\"\n\t| \"server\";\n\n/**\n * TTS tool provider configuration\n */\nexport interface TTSToolProviderConfig extends Partial<TTSConfig> {\n\t/**\n\t * TTS backend to use\n\t */\n\tbackend: TTSBackend;\n\n\t/**\n\t * Server API endpoint (for server backends)\n\t * @example '/api/tts/synthesize'\n\t * @example 'https://api.example.com/tts/synthesize'\n\t */\n\tapiEndpoint?: string;\n\n\t/**\n\t * Provider to use on server ('polly', 'google')\n\t * Only used when backend is 'server', 'polly', or 'google'\n\t */\n\tserverProvider?: \"polly\" | \"google\" | \"custom\";\n\n\t/**\n\t * Explicit transport mode for server provider payload translation.\n\t */\n\ttransportMode?: \"pie\" | \"custom\";\n\n\t/**\n\t * Endpoint style used by the server-backed provider.\n\t */\n\tendpointMode?: \"synthesizePath\" | \"rootPost\";\n\n\t/**\n\t * Endpoint validation strategy when validateEndpoint=true.\n\t */\n\tendpointValidationMode?: \"voices\" | \"endpoint\" | \"none\";\n\n\t/**\n\t * Include auth header when fetching remote audio/speech-mark assets.\n\t */\n\tincludeAuthOnAssetFetch?: boolean;\n\n\t/**\n\t * Auth token (if required)\n\t * Typically fetched via authFetcher in ToolProviderRegistry\n\t */\n\tauthToken?: string;\n\n\t/**\n\t * Organization ID for multi-tenant applications\n\t */\n\torganizationId?: string;\n\n\t/**\n\t * Default voice to use\n\t */\n\tvoice?: string;\n\n\t/**\n\t * Speech rate (0.25 to 4.0, default 1.0)\n\t */\n\trate?: number;\n\n\t/**\n\t * Speech pitch (0 to 2, default 1.0)\n\t * Note: Only browser backend supports pitch\n\t */\n\tpitch?: number;\n\n\t/**\n\t * Optional telemetry callback for tool/backend instrumentation.\n\t */\n\tonTelemetry?: (\n\t\teventName: string,\n\t\tpayload?: Record<string, unknown>,\n\t) => void | Promise<void>;\n}\n\n/**\n * TTS Tool Provider\n *\n * Wraps TTS providers (Browser, Polly, Google) with the ToolProviderApi interface\n * for use in the ToolProviderRegistry.\n *\n * @example Browser TTS (no auth)\n * ```typescript\n * const provider = new TTSToolProvider();\n * await provider.initialize({ backend: 'browser' });\n * const ttsProvider = await provider.createInstance();\n * ```\n *\n * @example Server TTS (with auth)\n * ```typescript\n * const provider = new TTSToolProvider();\n * await provider.initialize({\n * backend: 'polly',\n * apiEndpoint: '/api/tts/synthesize',\n * authToken: 'bearer-token', // Fetched via authFetcher\n * });\n * const ttsProvider = await provider.createInstance();\n * ```\n */\nexport class TTSToolProvider\n\timplements ToolProviderApi<TTSToolProviderConfig, ITTSProvider>\n{\n\treadonly providerId = \"tts-service\";\n\treadonly providerName = \"Text-to-Speech\";\n\treadonly category = \"tts\" as const;\n\treadonly version = \"1.0\";\n\treadonly requiresAuth: boolean;\n\n\tprivate ttsProvider: ITTSProvider | null = null;\n\tprivate config: TTSToolProviderConfig | null = null;\n\n\tprivate async emitTelemetry(\n\t\teventName: string,\n\t\tpayload?: Record<string, unknown>,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tawait this.config?.onTelemetry?.(eventName, payload);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[TTSToolProvider] telemetry callback failed:\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Create TTS tool provider\n\t *\n\t * @param backend TTS backend to use (default: 'browser')\n\t */\n\tconstructor(backend: TTSBackend = \"browser\") {\n\t\tthis.requiresAuth = backend !== \"browser\";\n\t}\n\n\t/**\n\t * Initialize TTS provider\n\t *\n\t * Sets up the appropriate TTS backend.\n\t *\n\t * @param config Configuration with backend type and credentials\n\t * @throws Error if initialization fails or required config missing\n\t */\n\tasync initialize(config: TTSToolProviderConfig): Promise<void> {\n\t\tif (this.ttsProvider) {\n\t\t\tconsole.warn(\n\t\t\t\t\"[TTSToolProvider] Already initialized, skipping reinitialization\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.config = config;\n\n\t\tswitch (config.backend) {\n\t\t\tcase \"browser\":\n\t\t\t\tawait this._initializeBrowserTTS(config);\n\t\t\t\tbreak;\n\n\t\t\tcase \"polly\":\n\t\t\tcase \"google\":\n\t\t\tcase \"server\":\n\t\t\t\tawait this._initializeServerTTS(config);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`[TTSToolProvider] Unknown backend: ${config.backend}`);\n\t\t}\n\n\t\tconsole.log(\n\t\t\t`[TTSToolProvider] Initialized successfully (backend: ${config.backend})`,\n\t\t);\n\t}\n\n\t/**\n\t * Initialize browser TTS (Web Speech API)\n\t */\n\tprivate async _initializeBrowserTTS(\n\t\tconfig: TTSToolProviderConfig,\n\t): Promise<void> {\n\t\t// Check if Web Speech API is available\n\t\tif (typeof window === \"undefined\" || !(\"speechSynthesis\" in window)) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[TTSToolProvider] Browser TTS not supported (Web Speech API not available)\",\n\t\t\t);\n\t\t}\n\n\t\tthis.ttsProvider = new BrowserTTSProvider();\n\t\tconsole.log(\"[TTSToolProvider] Browser TTS initialized (Web Speech API)\");\n\t}\n\n\t/**\n\t * Initialize server-based TTS (Polly, Google)\n\t */\n\tprivate async _initializeServerTTS(\n\t\tconfig: TTSToolProviderConfig,\n\t): Promise<void> {\n\t\tif (!config.apiEndpoint) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[TTSToolProvider] apiEndpoint required for server-based TTS backends\",\n\t\t\t);\n\t\t}\n\n\t\t// Server backends are optional; load implementation only when requested.\n\t\tconst moduleLoadStartedAt = Date.now();\n\t\tawait this.emitTelemetry(\"pie-tool-library-load-start\", {\n\t\t\ttoolId: \"textToSpeech\",\n\t\t\toperation: \"server-provider-module-import\",\n\t\t\tbackend: config.serverProvider || config.backend,\n\t\t});\n\t\tconst serverModule = await (async () => {\n\t\t\ttry {\n\t\t\t\tconst loaded = (await import(\"@pie-players/tts-client-server\")) as {\n\t\t\t\t\tServerTTSProvider: new () => ITTSProvider;\n\t\t\t\t};\n\t\t\t\tawait this.emitTelemetry(\"pie-tool-library-load-success\", {\n\t\t\t\t\ttoolId: \"textToSpeech\",\n\t\t\t\t\toperation: \"server-provider-module-import\",\n\t\t\t\t\tbackend: config.serverProvider || config.backend,\n\t\t\t\t\tduration: Date.now() - moduleLoadStartedAt,\n\t\t\t\t});\n\t\t\t\treturn loaded;\n\t\t\t} catch (error) {\n\t\t\t\tawait this.emitTelemetry(\"pie-tool-library-load-error\", {\n\t\t\t\ttoolId: \"textToSpeech\",\n\t\t\t\t\toperation: \"server-provider-module-import\",\n\t\t\t\t\tbackend: config.serverProvider || config.backend,\n\t\t\t\t\tduration: Date.now() - moduleLoadStartedAt,\n\t\t\t\t\terrorType: \"ToolLibraryLoadError\",\n\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t})();\n\t\tthis.ttsProvider = new serverModule.ServerTTSProvider();\n\n\t\tconsole.log(\n\t\t\t`[TTSToolProvider] Server TTS initialized (provider: ${config.serverProvider || config.backend})`,\n\t\t);\n\t}\n\n\t/**\n\t * Create a TTS provider instance\n\t *\n\t * Returns the initialized TTS provider.\n\t *\n\t * @param config Optional instance-specific configuration (currently unused)\n\t * @returns TTS provider\n\t * @throws Error if provider not initialized\n\t */\n\tasync createInstance(\n\t\tconfig?: Partial<TTSToolProviderConfig>,\n\t): Promise<ITTSProvider> {\n\t\tif (!this.ttsProvider) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[TTSToolProvider] Provider not initialized. Call initialize() first.\",\n\t\t\t);\n\t\t}\n\n\t\treturn this.ttsProvider;\n\t}\n\n\t/**\n\t * Get provider capabilities\n\t *\n\t * @returns TTS capabilities based on backend\n\t */\n\tgetCapabilities(): ToolProviderCapabilities {\n\t\tconst isBrowser = this.config?.backend === \"browser\";\n\n\t\treturn {\n\t\t\tsupportsOffline: isBrowser,\n\t\t\trequiresAuth: !isBrowser,\n\t\t\tmaxInstances: 1, // Single TTS instance (playback is sequential)\n\t\t\tfeatures: {\n\t\t\t\twordBoundary: true, // All backends support word highlighting\n\t\t\t\tpause: true,\n\t\t\t\tresume: true,\n\t\t\t\trateControl: true,\n\t\t\t\tpitchControl: isBrowser, // Only browser supports pitch\n\t\t\t\tvoiceSelection: true,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Check if provider is ready\n\t *\n\t * @returns true if provider is initialized\n\t */\n\tisReady(): boolean {\n\t\treturn this.ttsProvider !== null;\n\t}\n\n\t/**\n\t * Clean up provider resources\n\t *\n\t * Destroys the TTS provider and releases resources.\n\t */\n\tdestroy(): void {\n\t\tif (this.ttsProvider) {\n\t\t\tthis.ttsProvider.destroy();\n\t\t\tthis.ttsProvider = null;\n\t\t}\n\t\tthis.config = null;\n\t\tconsole.log(\"[TTSToolProvider] Destroyed\");\n\t}\n}\n"]}
1
+ {"version":3,"file":"TTSToolProvider.js","sourceRoot":"","sources":["../../../src/services/tool-providers/TTSToolProvider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TTS Tool Provider\n *\n * Unified provider for all TTS backends:\n * - Browser Web Speech API (no auth)\n * - AWS Polly (requires auth via server proxy)\n * - Google Cloud TTS (requires auth via server proxy)\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type { ITTSProvider, TTSConfig } from \"@pie-players/pie-tts\";\nimport { BrowserTTSProvider } from \"../../services/tts/browser-provider.js\";\nimport type {\n\tToolProviderApi,\n\tToolProviderCapabilities,\n} from \"./ToolProviderApi.js\";\n\n/**\n * TTS backend type\n */\nexport type TTSBackend = \"browser\" | \"polly\" | \"google\" | \"server\";\n\n/**\n * TTS tool provider configuration\n */\nexport interface TTSToolProviderConfig extends Partial<TTSConfig> {\n\t/**\n\t * TTS backend to use\n\t */\n\tbackend: TTSBackend;\n\n\t/**\n\t * Server API endpoint (for server backends)\n\t * @example '/api/tts/synthesize'\n\t * @example 'https://api.example.com/tts/synthesize'\n\t */\n\tapiEndpoint?: string;\n\n\t/**\n\t * Provider to use on server ('polly', 'google')\n\t * Only used when backend is 'server', 'polly', or 'google'\n\t */\n\tserverProvider?: \"polly\" | \"google\" | \"custom\";\n\n\t/**\n\t * Explicit transport mode for server provider payload translation.\n\t */\n\ttransportMode?: \"pie\" | \"custom\";\n\n\t/**\n\t * Endpoint style used by the server-backed provider.\n\t */\n\tendpointMode?: \"synthesizePath\" | \"rootPost\";\n\n\t/**\n\t * Endpoint validation strategy when validateEndpoint=true.\n\t */\n\tendpointValidationMode?: \"voices\" | \"endpoint\" | \"none\";\n\n\t/**\n\t * Include auth header when fetching remote audio/speech-mark assets.\n\t */\n\tincludeAuthOnAssetFetch?: boolean;\n\n\t/**\n\t * Auth token (if required)\n\t * Typically fetched via authFetcher in ToolProviderRegistry\n\t */\n\tauthToken?: string;\n\n\t/**\n\t * Organization ID for multi-tenant applications\n\t */\n\torganizationId?: string;\n\n\t/**\n\t * Default voice to use\n\t */\n\tvoice?: string;\n\n\t/**\n\t * Speech rate (0.25 to 4.0, default 1.0)\n\t */\n\trate?: number;\n\n\t/**\n\t * Speech pitch (0 to 2, default 1.0)\n\t * Note: Only browser backend supports pitch\n\t */\n\tpitch?: number;\n\n\t/**\n\t * Optional telemetry callback for tool/backend instrumentation.\n\t */\n\tonTelemetry?: (\n\t\teventName: string,\n\t\tpayload?: Record<string, unknown>,\n\t) => void | Promise<void>;\n}\n\n/**\n * TTS Tool Provider\n *\n * Wraps TTS providers (Browser, Polly, Google) with the ToolProviderApi interface\n * for use in the ToolProviderRegistry.\n *\n * @example Browser TTS (no auth)\n * ```typescript\n * const provider = new TTSToolProvider();\n * await provider.initialize({ backend: 'browser' });\n * const ttsProvider = await provider.createInstance();\n * ```\n *\n * @example Server TTS (with auth)\n * ```typescript\n * const provider = new TTSToolProvider();\n * await provider.initialize({\n * backend: 'polly',\n * apiEndpoint: '/api/tts/synthesize',\n * authToken: 'bearer-token', // Fetched via authFetcher\n * });\n * const ttsProvider = await provider.createInstance();\n * ```\n */\nexport class TTSToolProvider\n\timplements ToolProviderApi<TTSToolProviderConfig, ITTSProvider>\n{\n\treadonly providerId = \"tts-service\";\n\treadonly providerName = \"Text-to-Speech\";\n\treadonly category = \"tts\" as const;\n\treadonly version = \"1.0\";\n\treadonly requiresAuth: boolean;\n\n\tprivate ttsProvider: ITTSProvider | null = null;\n\tprivate config: TTSToolProviderConfig | null = null;\n\n\tprivate async emitTelemetry(\n\t\teventName: string,\n\t\tpayload?: Record<string, unknown>,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tawait this.config?.onTelemetry?.(eventName, payload);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"[TTSToolProvider] telemetry callback failed:\", error);\n\t\t}\n\t}\n\n\t/**\n\t * Create TTS tool provider\n\t *\n\t * @param backend TTS backend to use (default: 'browser')\n\t */\n\tconstructor(backend: TTSBackend = \"browser\") {\n\t\tthis.requiresAuth = backend !== \"browser\";\n\t}\n\n\t/**\n\t * Initialize TTS provider\n\t *\n\t * Sets up the appropriate TTS backend.\n\t *\n\t * @param config Configuration with backend type and credentials\n\t * @throws Error if initialization fails or required config missing\n\t */\n\tasync initialize(config: TTSToolProviderConfig): Promise<void> {\n\t\tif (this.ttsProvider) {\n\t\t\tconsole.warn(\n\t\t\t\t\"[TTSToolProvider] Already initialized, skipping reinitialization\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.config = config;\n\n\t\tswitch (config.backend) {\n\t\t\tcase \"browser\":\n\t\t\t\tawait this._initializeBrowserTTS(config);\n\t\t\t\tbreak;\n\n\t\t\tcase \"polly\":\n\t\t\tcase \"google\":\n\t\t\tcase \"server\":\n\t\t\t\tawait this._initializeServerTTS(config);\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tthrow new Error(`[TTSToolProvider] Unknown backend: ${config.backend}`);\n\t\t}\n\n\t\tconsole.log(\n\t\t\t`[TTSToolProvider] Initialized successfully (backend: ${config.backend})`,\n\t\t);\n\t}\n\n\t/**\n\t * Initialize browser TTS (Web Speech API)\n\t */\n\tprivate async _initializeBrowserTTS(\n\t\tconfig: TTSToolProviderConfig,\n\t): Promise<void> {\n\t\t// Check if Web Speech API is available\n\t\tif (typeof window === \"undefined\" || !(\"speechSynthesis\" in window)) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[TTSToolProvider] Browser TTS not supported (Web Speech API not available)\",\n\t\t\t);\n\t\t}\n\n\t\tthis.ttsProvider = new BrowserTTSProvider();\n\t\tconsole.log(\"[TTSToolProvider] Browser TTS initialized (Web Speech API)\");\n\t}\n\n\t/**\n\t * Initialize server-based TTS (Polly, Google)\n\t */\n\tprivate async _initializeServerTTS(\n\t\tconfig: TTSToolProviderConfig,\n\t): Promise<void> {\n\t\tif (!config.apiEndpoint) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[TTSToolProvider] apiEndpoint required for server-based TTS backends\",\n\t\t\t);\n\t\t}\n\n\t\t// Server backends are optional; load implementation only when requested.\n\t\tconst moduleLoadStartedAt = Date.now();\n\t\tawait this.emitTelemetry(\"pie-tool-library-load-start\", {\n\t\t\ttoolId: \"textToSpeech\",\n\t\t\toperation: \"server-provider-module-import\",\n\t\t\tbackend: config.serverProvider || config.backend,\n\t\t});\n\t\tconst serverModule = await (async () => {\n\t\t\ttry {\n\t\t\t\tconst loaded = (await import(\"@pie-players/tts-client-server\")) as {\n\t\t\t\t\tServerTTSProvider: new () => ITTSProvider;\n\t\t\t\t};\n\t\t\t\tawait this.emitTelemetry(\"pie-tool-library-load-success\", {\n\t\t\t\t\ttoolId: \"textToSpeech\",\n\t\t\t\t\toperation: \"server-provider-module-import\",\n\t\t\t\t\tbackend: config.serverProvider || config.backend,\n\t\t\t\t\tduration: Date.now() - moduleLoadStartedAt,\n\t\t\t\t});\n\t\t\t\treturn loaded;\n\t\t\t} catch (error) {\n\t\t\t\tawait this.emitTelemetry(\"pie-tool-library-load-error\", {\n\t\t\t\t\ttoolId: \"textToSpeech\",\n\t\t\t\t\toperation: \"server-provider-module-import\",\n\t\t\t\t\tbackend: config.serverProvider || config.backend,\n\t\t\t\t\tduration: Date.now() - moduleLoadStartedAt,\n\t\t\t\t\terrorType: \"ToolLibraryLoadError\",\n\t\t\t\t\tmessage: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t})();\n\t\tthis.ttsProvider = new serverModule.ServerTTSProvider();\n\n\t\tconsole.log(\n\t\t\t`[TTSToolProvider] Server TTS initialized (provider: ${config.serverProvider || config.backend})`,\n\t\t);\n\t}\n\n\t/**\n\t * Create a TTS provider instance\n\t *\n\t * Returns the initialized TTS provider.\n\t *\n\t * @param config Optional instance-specific configuration (currently unused)\n\t * @returns TTS provider\n\t * @throws Error if provider not initialized\n\t */\n\tasync createInstance(\n\t\tconfig?: Partial<TTSToolProviderConfig>,\n\t): Promise<ITTSProvider> {\n\t\tif (!this.ttsProvider) {\n\t\t\tthrow new Error(\n\t\t\t\t\"[TTSToolProvider] Provider not initialized. Call initialize() first.\",\n\t\t\t);\n\t\t}\n\n\t\treturn this.ttsProvider;\n\t}\n\n\t/**\n\t * Get provider capabilities\n\t *\n\t * @returns TTS capabilities based on backend\n\t */\n\tgetCapabilities(): ToolProviderCapabilities {\n\t\tconst isBrowser = this.config?.backend === \"browser\";\n\n\t\treturn {\n\t\t\tsupportsOffline: isBrowser,\n\t\t\trequiresAuth: !isBrowser,\n\t\t\tmaxInstances: 1, // Single TTS instance (playback is sequential)\n\t\t\tfeatures: {\n\t\t\t\twordBoundary: true, // All backends support word highlighting\n\t\t\t\tpause: true,\n\t\t\t\tresume: true,\n\t\t\t\trateControl: true,\n\t\t\t\tpitchControl: isBrowser, // Only browser supports pitch\n\t\t\t\tvoiceSelection: true,\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Check if provider is ready\n\t *\n\t * @returns true if provider is initialized\n\t */\n\tisReady(): boolean {\n\t\treturn this.ttsProvider !== null;\n\t}\n\n\t/**\n\t * Clean up provider resources\n\t *\n\t * Destroys the TTS provider and releases resources.\n\t */\n\tdestroy(): void {\n\t\tif (this.ttsProvider) {\n\t\t\tthis.ttsProvider.destroy();\n\t\t\tthis.ttsProvider = null;\n\t\t}\n\t\tthis.config = null;\n\t\tconsole.log(\"[TTSToolProvider] Destroyed\");\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar-items.js","sourceRoot":"","sources":["../../src/services/toolbar-items.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,iBAAiB,CAAC,IAAiB;IAClD,OAAO,OAAQ,IAAwB,CAAC,IAAI,KAAK,QAAQ,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACrD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,MAAM,IAAI,GAAG,KAAgC,CAAC;IAC9C,IAAI,OAAO,IAAI,CAAC,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7E,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACnF,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACrF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC3E,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACjF,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAChF,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAEpF,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC;IAC9C,MAAM,UAAU,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU,CAAC;IACtD,IAAI,OAAO,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,OAAO;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,UAAU,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAwB;IACvD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACzD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC","sourcesContent":["export interface ToolbarItemBase {\n\tid: string;\n\tlabel: string;\n\tariaLabel?: string;\n\ticon?: string;\n\ttooltip?: string;\n\tactive?: boolean;\n\tdisabled?: boolean;\n}\n\nexport interface ToolbarButtonItem extends ToolbarItemBase {\n\tonClick: () => void;\n\thref?: never;\n\ttarget?: never;\n\trel?: never;\n}\n\nexport interface ToolbarLinkItem extends ToolbarItemBase {\n\thref: string;\n\ttarget?: \"_self\" | \"_blank\" | \"_parent\" | \"_top\" | string;\n\trel?: string;\n\tonClick?: never;\n}\n\nexport type ToolbarItem = ToolbarButtonItem | ToolbarLinkItem;\n\nexport function isToolbarLinkItem(item: ToolbarItem): item is ToolbarLinkItem {\n\treturn typeof (item as ToolbarLinkItem).href === \"string\";\n}\n\nexport function isValidToolbarItemShape(value: unknown): value is ToolbarItem {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst item = value as Record<string, unknown>;\n\tif (typeof item.id !== \"string\" || item.id.trim().length === 0) return false;\n\tif (typeof item.label !== \"string\" || item.label.trim().length === 0) return false;\n\tif (item.ariaLabel !== undefined && typeof item.ariaLabel !== \"string\") return false;\n\tif (item.icon !== undefined && typeof item.icon !== \"string\") return false;\n\tif (item.tooltip !== undefined && typeof item.tooltip !== \"string\") return false;\n\tif (item.active !== undefined && typeof item.active !== \"boolean\") return false;\n\tif (item.disabled !== undefined && typeof item.disabled !== \"boolean\") return false;\n\n\tconst hasHref = typeof item.href === \"string\";\n\tconst hasOnClick = typeof item.onClick === \"function\";\n\tif (hasHref === hasOnClick) return false;\n\tif (hasHref) return true;\n\treturn hasOnClick;\n}\n\nexport function isInlineSvgIcon(icon: string | undefined): boolean {\n\treturn typeof icon === \"string\" && icon.trimStart().startsWith(\"<svg\");\n}\n\nexport function isExternalIconUrl(icon: string | undefined): boolean {\n\treturn typeof icon === \"string\" && icon.trimStart().startsWith(\"http\");\n}\n"]}
1
+ {"version":3,"file":"toolbar-items.js","sourceRoot":"","sources":["../../src/services/toolbar-items.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,iBAAiB,CAAC,IAAiB;IAClD,OAAO,OAAQ,IAAwB,CAAC,IAAI,KAAK,QAAQ,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACrD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,MAAM,IAAI,GAAG,KAAgC,CAAC;IAC9C,IAAI,OAAO,IAAI,CAAC,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7E,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QACnE,OAAO,KAAK,CAAC;IACd,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ;QACrE,OAAO,KAAK,CAAC;IACd,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC3E,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;QACjE,OAAO,KAAK,CAAC;IACd,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,SAAS;QAChE,OAAO,KAAK,CAAC;IACd,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,SAAS;QACpE,OAAO,KAAK,CAAC;IAEd,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC;IAC9C,MAAM,UAAU,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU,CAAC;IACtD,IAAI,OAAO,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,OAAO;QAAE,OAAO,IAAI,CAAC;IACzB,OAAO,UAAU,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAwB;IACvD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACzD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC","sourcesContent":["export interface ToolbarItemBase {\n\tid: string;\n\tlabel: string;\n\tariaLabel?: string;\n\ticon?: string;\n\ttooltip?: string;\n\tactive?: boolean;\n\tdisabled?: boolean;\n}\n\nexport interface ToolbarButtonItem extends ToolbarItemBase {\n\tonClick: () => void;\n\thref?: never;\n\ttarget?: never;\n\trel?: never;\n}\n\nexport interface ToolbarLinkItem extends ToolbarItemBase {\n\thref: string;\n\ttarget?: \"_self\" | \"_blank\" | \"_parent\" | \"_top\" | string;\n\trel?: string;\n\tonClick?: never;\n}\n\nexport type ToolbarItem = ToolbarButtonItem | ToolbarLinkItem;\n\nexport function isToolbarLinkItem(item: ToolbarItem): item is ToolbarLinkItem {\n\treturn typeof (item as ToolbarLinkItem).href === \"string\";\n}\n\nexport function isValidToolbarItemShape(value: unknown): value is ToolbarItem {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst item = value as Record<string, unknown>;\n\tif (typeof item.id !== \"string\" || item.id.trim().length === 0) return false;\n\tif (typeof item.label !== \"string\" || item.label.trim().length === 0)\n\t\treturn false;\n\tif (item.ariaLabel !== undefined && typeof item.ariaLabel !== \"string\")\n\t\treturn false;\n\tif (item.icon !== undefined && typeof item.icon !== \"string\") return false;\n\tif (item.tooltip !== undefined && typeof item.tooltip !== \"string\")\n\t\treturn false;\n\tif (item.active !== undefined && typeof item.active !== \"boolean\")\n\t\treturn false;\n\tif (item.disabled !== undefined && typeof item.disabled !== \"boolean\")\n\t\treturn false;\n\n\tconst hasHref = typeof item.href === \"string\";\n\tconst hasOnClick = typeof item.onClick === \"function\";\n\tif (hasHref === hasOnClick) return false;\n\tif (hasHref) return true;\n\treturn hasOnClick;\n}\n\nexport function isInlineSvgIcon(icon: string | undefined): boolean {\n\treturn typeof icon === \"string\" && icon.trimStart().startsWith(\"<svg\");\n}\n\nexport function isExternalIconUrl(icon: string | undefined): boolean {\n\treturn typeof icon === \"string\" && icon.trimStart().startsWith(\"http\");\n}\n"]}
@@ -126,8 +126,7 @@ export function normalizeToolsConfig(input) {
126
126
  section: normalizeToolList(assertStringArray(placement?.section, "placement.section")).length
127
127
  ? normalizeToolList(assertStringArray(placement?.section, "placement.section"))
128
128
  : [...DEFAULT_PLACEMENT.section],
129
- item: normalizeToolList(assertStringArray(placement?.item, "placement.item"))
130
- .length
129
+ item: normalizeToolList(assertStringArray(placement?.item, "placement.item")).length
131
130
  ? normalizeToolList(assertStringArray(placement?.item, "placement.item"))
132
131
  : [...DEFAULT_PLACEMENT.item],
133
132
  passage: normalizeToolList(assertStringArray(placement?.passage, "placement.passage")).length
@@ -1 +1 @@
1
- {"version":3,"file":"tools-config-normalizer.js","sourceRoot":"","sources":["../../src/services/tools-config-normalizer.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_TOOL_PLACEMENT } from \"./tool-config-defaults.js\";\nimport type { SREMathSpeechOptions } from \"./tts/math-speech.js\";\n\nexport type ToolPlacementLevel = \"section\" | \"item\" | \"passage\";\n\nexport interface ToolPolicyConfig {\n\tallowed?: string[];\n\tblocked?: string[];\n}\n\nexport interface ToolPlacementConfig {\n\tsection?: string[];\n\titem?: string[];\n\tpassage?: string[];\n}\n\nexport interface ToolRuntimeBackendRequest {\n\tpath: string;\n\tmethod?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\theaders?: Record<string, string>;\n\tquery?: Record<string, string | number | boolean>;\n\tbody?: unknown;\n}\n\nexport interface ToolRuntimeProviderBridge {\n\t/**\n\t * Optional auth fetch hook for provider initialization.\n\t * Useful for retrieving temporary credentials from a host backend.\n\t */\n\tauthFetcher?: () => Promise<Record<string, unknown>>;\n\t/**\n\t * Optional backend request bridge for rich tool interactions\n\t * beyond one-time auth fetching.\n\t */\n\trequest?: (request: ToolRuntimeBackendRequest) => Promise<unknown>;\n\t/**\n\t * Optional host event emitter.\n\t */\n\temit?: (eventName: string, payload?: Record<string, unknown>) => void | Promise<void>;\n\t/**\n\t * Optional host event subscription bridge.\n\t */\n\tsubscribe?: (\n\t\teventName: string,\n\t\thandler: (payload: unknown) => void,\n\t) => (() => void) | undefined;\n\t[key: string]: unknown;\n}\n\nexport interface ToolRuntimeProviderConfig {\n\tid?: string;\n\tinit?: Record<string, unknown>;\n\truntime?: ToolRuntimeProviderBridge;\n}\n\nexport type TextToSpeechRuntimeProviderId = \"polly\" | \"google\" | \"custom\";\n\nexport interface ToolProviderConfig {\n\tenabled?: boolean;\n\tprovider?: ToolRuntimeProviderConfig;\n\tsettings?: Record<string, unknown>;\n\t[key: string]: unknown;\n}\n\nexport interface TextToSpeechToolProviderSettings\n\textends Record<string, unknown> {\n\tmathSpeech?: SREMathSpeechOptions;\n}\n\nexport interface TextToSpeechToolProviderConfig\n\textends Omit<ToolProviderConfig, \"provider\" | \"settings\"> {\n\tprovider?: ToolRuntimeProviderConfig | TextToSpeechRuntimeProviderId;\n\tserverProvider?: TextToSpeechRuntimeProviderId;\n\tmathSpeech?: SREMathSpeechOptions;\n\tsettings?: TextToSpeechToolProviderSettings;\n}\n\nexport interface ToolProvidersConfig {\n\ttextToSpeech?: TextToSpeechToolProviderConfig;\n\t[key: string]: ToolProviderConfig | TextToSpeechToolProviderConfig | undefined;\n}\n\n/**\n * PNP/profile enforcement mode mirrored on `runtime.tools.pnpEnforcement`\n * (M5 mirror rule). Carried alongside `policy` / `placement` /\n * `providers` so hosts can pin or opt out of PNP/profile precedence via\n * the runtime config without reaching for the toolkit prop directly.\n *\n * - `\"on\"` — force enforcement (engine applies PNP/profile gates).\n * - `\"off\"` — opt out (engine ignores PNP/profile inputs).\n * - omitted / `undefined` — auto-mode (the default). The toolkit\n * coordinator computes the effective mode from PNP/profile policy material on the\n * bound `AssessmentEntity` / `AssessmentItemRef`. See\n * `resolveDefaultPnpEnforcement` in `policy/core/pnp-policy-inputs.ts`.\n */\nexport type ToolsPnpEnforcement = \"on\" | \"off\";\n\nexport interface CanonicalToolsConfig {\n\tpolicy: ToolPolicyConfig;\n\tplacement: Required<ToolPlacementConfig>;\n\tproviders: ToolProvidersConfig;\n\t/**\n\t * Optional PNP/profile enforcement override. Mirrored on\n\t * `runtime.tools.pnpEnforcement`; consumed by\n\t * `<pie-assessment-toolkit>` and `ToolkitCoordinator` as the\n\t * embedded path's equivalent of the standalone `pnp-enforcement`\n\t * attribute. See {@link ToolsPnpEnforcement} for semantics.\n\t */\n\tpnpEnforcement?: ToolsPnpEnforcement;\n}\n\nconst DEFAULT_PLACEMENT: Required<ToolPlacementConfig> = {\n\tsection: [...DEFAULT_TOOL_PLACEMENT.section],\n\titem: [...DEFAULT_TOOL_PLACEMENT.item],\n\tpassage: [...DEFAULT_TOOL_PLACEMENT.passage],\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction assertStringArray(value: unknown, fieldPath: string): string[] {\n\tif (value == null) return [];\n\tif (!Array.isArray(value)) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"${fieldPath}\": expected an array of tool ids.`,\n\t\t);\n\t}\n\tconst invalidEntry = value.find((entry) => typeof entry !== \"string\");\n\tif (invalidEntry !== undefined) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"${fieldPath}\": all entries must be strings.`,\n\t\t);\n\t}\n\treturn value as string[];\n}\n\nfunction assertPlacementConfig(value: unknown): ToolPlacementConfig | undefined {\n\tif (value == null) return undefined;\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config at \"placement\": expected an object with section/item/passage arrays.',\n\t\t);\n\t}\n\treturn value as ToolPlacementConfig;\n}\n\nfunction assertPolicyConfig(value: unknown): ToolPolicyConfig | undefined {\n\tif (value == null) return undefined;\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config at \"policy\": expected an object with allowed/blocked arrays.',\n\t\t);\n\t}\n\treturn value as ToolPolicyConfig;\n}\n\nfunction assertProviderConfig(\n\tproviderId: string,\n\tvalue: unknown,\n): ToolProviderConfig | TextToSpeechToolProviderConfig | undefined {\n\tif (value == null) return undefined;\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}\": expected an object.`,\n\t\t);\n\t}\n\tconst config = value as ToolProviderConfig;\n\tconst isTTSRuntimeProviderId = (provider: unknown): boolean =>\n\t\tprovider === \"polly\" || provider === \"google\" || provider === \"custom\";\n\tif (\n\t\t\"enabled\" in config &&\n\t\tconfig.enabled !== undefined &&\n\t\ttypeof config.enabled !== \"boolean\"\n\t) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}.enabled\": expected a boolean.`,\n\t\t);\n\t}\n\tif (\n\t\t\"settings\" in config &&\n\t\tconfig.settings !== undefined &&\n\t\t!isPlainObject(config.settings)\n\t) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}.settings\": expected an object.`,\n\t\t);\n\t}\n\tif (\n\t\t\"provider\" in config &&\n\t\tconfig.provider !== undefined &&\n\t\t!isPlainObject(config.provider) &&\n\t\t!(providerId === \"textToSpeech\" && isTTSRuntimeProviderId(config.provider))\n\t) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}.provider\": expected an object.`,\n\t\t);\n\t}\n\treturn config;\n}\n\nfunction assertProvidersConfig(value: unknown): ToolProvidersConfig {\n\tif (value == null) return {};\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config at \"providers\": expected an object keyed by tool id.',\n\t\t);\n\t}\n\tconst normalized: ToolProvidersConfig = {};\n\tfor (const [providerId, providerConfig] of Object.entries(value)) {\n\t\tnormalized[providerId] = assertProviderConfig(providerId, providerConfig);\n\t}\n\treturn normalized;\n}\n\nexport function normalizeToolAlias(toolId: string): string {\n\tconst trimmed = toolId.trim();\n\tif (!trimmed) return \"\";\n\treturn trimmed;\n}\n\nexport function normalizeToolList(toolIds: string[] | undefined | null): string[] {\n\tif (!toolIds || toolIds.length === 0) return [];\n\tconst deduped = new Set<string>();\n\tfor (const rawToolId of toolIds) {\n\t\tif (typeof rawToolId !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t\"Invalid tools config: tool list entries must be strings.\",\n\t\t\t);\n\t\t}\n\t\tconst normalized = normalizeToolAlias(rawToolId);\n\t\tif (!normalized) continue;\n\t\tdeduped.add(normalized);\n\t}\n\treturn Array.from(deduped);\n}\n\nexport function parseToolList(input: string | undefined | null): string[] {\n\tif (!input) return [];\n\treturn normalizeToolList(\n\t\tinput\n\t\t\t.split(\",\")\n\t\t\t.map((entry) => entry.trim())\n\t\t\t.filter(Boolean),\n\t);\n}\n\nfunction assertPnpEnforcement(\n\tvalue: unknown,\n\tfieldPath: \"pnpEnforcement\",\n): ToolsPnpEnforcement | undefined {\n\tif (value == null) return undefined;\n\tif (value === \"on\" || value === \"off\") return value;\n\tthrow new Error(\n\t\t`Invalid tools config at \"${fieldPath}\": expected \"on\" or \"off\", got ${JSON.stringify(value)}.`,\n\t);\n}\n\nexport function normalizeToolsConfig(\n\tinput?: Partial<CanonicalToolsConfig> | null,\n): CanonicalToolsConfig {\n\tif (input != null && !isPlainObject(input)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config: expected an object with \"policy\", \"placement\", and \"providers\".',\n\t\t);\n\t}\n\tconst policy = assertPolicyConfig(input?.policy);\n\tconst placement = assertPlacementConfig(input?.placement);\n\tconst providers = assertProvidersConfig(input?.providers);\n\tconst pnpEnforcement = assertPnpEnforcement(\n\t\tinput?.pnpEnforcement,\n\t\t\"pnpEnforcement\",\n\t);\n\n\tconst config: CanonicalToolsConfig = {\n\t\tpolicy: {\n\t\t\tallowed: normalizeToolList(assertStringArray(policy?.allowed, \"policy.allowed\")),\n\t\t\tblocked: normalizeToolList(assertStringArray(policy?.blocked, \"policy.blocked\")),\n\t\t},\n\t\tplacement: {\n\t\t\tsection: normalizeToolList(\n\t\t\t\tassertStringArray(placement?.section, \"placement.section\"),\n\t\t\t).length\n\t\t\t\t? normalizeToolList(assertStringArray(placement?.section, \"placement.section\"))\n\t\t\t\t: [...DEFAULT_PLACEMENT.section],\n\t\t\titem: normalizeToolList(assertStringArray(placement?.item, \"placement.item\"))\n\t\t\t\t.length\n\t\t\t\t? normalizeToolList(assertStringArray(placement?.item, \"placement.item\"))\n\t\t\t\t: [...DEFAULT_PLACEMENT.item],\n\t\t\tpassage: normalizeToolList(\n\t\t\t\tassertStringArray(placement?.passage, \"placement.passage\"),\n\t\t\t).length\n\t\t\t\t? normalizeToolList(assertStringArray(placement?.passage, \"placement.passage\"))\n\t\t\t\t: [...DEFAULT_PLACEMENT.passage],\n\t\t},\n\t\tproviders,\n\t};\n\tif (pnpEnforcement) {\n\t\tconfig.pnpEnforcement = pnpEnforcement;\n\t}\n\treturn config;\n}\n\n"]}
1
+ {"version":3,"file":"tools-config-normalizer.js","sourceRoot":"","sources":["../../src/services/tools-config-normalizer.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_TOOL_PLACEMENT } from \"./tool-config-defaults.js\";\nimport type { SREMathSpeechOptions } from \"./tts/math-speech.js\";\n\nexport type ToolPlacementLevel = \"section\" | \"item\" | \"passage\";\n\nexport interface ToolPolicyConfig {\n\tallowed?: string[];\n\tblocked?: string[];\n}\n\nexport interface ToolPlacementConfig {\n\tsection?: string[];\n\titem?: string[];\n\tpassage?: string[];\n}\n\nexport interface ToolRuntimeBackendRequest {\n\tpath: string;\n\tmethod?: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\theaders?: Record<string, string>;\n\tquery?: Record<string, string | number | boolean>;\n\tbody?: unknown;\n}\n\nexport interface ToolRuntimeProviderBridge {\n\t/**\n\t * Optional auth fetch hook for provider initialization.\n\t * Useful for retrieving temporary credentials from a host backend.\n\t */\n\tauthFetcher?: () => Promise<Record<string, unknown>>;\n\t/**\n\t * Optional backend request bridge for rich tool interactions\n\t * beyond one-time auth fetching.\n\t */\n\trequest?: (request: ToolRuntimeBackendRequest) => Promise<unknown>;\n\t/**\n\t * Optional host event emitter.\n\t */\n\temit?: (\n\t\teventName: string,\n\t\tpayload?: Record<string, unknown>,\n\t) => void | Promise<void>;\n\t/**\n\t * Optional host event subscription bridge.\n\t */\n\tsubscribe?: (\n\t\teventName: string,\n\t\thandler: (payload: unknown) => void,\n\t) => (() => void) | undefined;\n\t[key: string]: unknown;\n}\n\nexport interface ToolRuntimeProviderConfig {\n\tid?: string;\n\tinit?: Record<string, unknown>;\n\truntime?: ToolRuntimeProviderBridge;\n}\n\nexport type TextToSpeechRuntimeProviderId = \"polly\" | \"google\" | \"custom\";\n\nexport interface ToolProviderConfig {\n\tenabled?: boolean;\n\tprovider?: ToolRuntimeProviderConfig;\n\tsettings?: Record<string, unknown>;\n\t[key: string]: unknown;\n}\n\nexport interface TextToSpeechToolProviderSettings\n\textends Record<string, unknown> {\n\tmathSpeech?: SREMathSpeechOptions;\n}\n\nexport interface TextToSpeechToolProviderConfig\n\textends Omit<ToolProviderConfig, \"provider\" | \"settings\"> {\n\tprovider?: ToolRuntimeProviderConfig | TextToSpeechRuntimeProviderId;\n\tserverProvider?: TextToSpeechRuntimeProviderId;\n\tmathSpeech?: SREMathSpeechOptions;\n\tsettings?: TextToSpeechToolProviderSettings;\n}\n\nexport interface ToolProvidersConfig {\n\ttextToSpeech?: TextToSpeechToolProviderConfig;\n\t[key: string]:\n\t\t| ToolProviderConfig\n\t\t| TextToSpeechToolProviderConfig\n\t\t| undefined;\n}\n\n/**\n * PNP/profile enforcement mode mirrored on `runtime.tools.pnpEnforcement`\n * (M5 mirror rule). Carried alongside `policy` / `placement` /\n * `providers` so hosts can pin or opt out of PNP/profile precedence via\n * the runtime config without reaching for the toolkit prop directly.\n *\n * - `\"on\"` — force enforcement (engine applies PNP/profile gates).\n * - `\"off\"` — opt out (engine ignores PNP/profile inputs).\n * - omitted / `undefined` — auto-mode (the default). The toolkit\n * coordinator computes the effective mode from PNP/profile policy material on the\n * bound `AssessmentEntity` / `AssessmentItemRef`. See\n * `resolveDefaultPnpEnforcement` in `policy/core/pnp-policy-inputs.ts`.\n */\nexport type ToolsPnpEnforcement = \"on\" | \"off\";\n\nexport interface CanonicalToolsConfig {\n\tpolicy: ToolPolicyConfig;\n\tplacement: Required<ToolPlacementConfig>;\n\tproviders: ToolProvidersConfig;\n\t/**\n\t * Optional PNP/profile enforcement override. Mirrored on\n\t * `runtime.tools.pnpEnforcement`; consumed by\n\t * `<pie-assessment-toolkit>` and `ToolkitCoordinator` as the\n\t * embedded path's equivalent of the standalone `pnp-enforcement`\n\t * attribute. See {@link ToolsPnpEnforcement} for semantics.\n\t */\n\tpnpEnforcement?: ToolsPnpEnforcement;\n}\n\nconst DEFAULT_PLACEMENT: Required<ToolPlacementConfig> = {\n\tsection: [...DEFAULT_TOOL_PLACEMENT.section],\n\titem: [...DEFAULT_TOOL_PLACEMENT.item],\n\tpassage: [...DEFAULT_TOOL_PLACEMENT.passage],\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction assertStringArray(value: unknown, fieldPath: string): string[] {\n\tif (value == null) return [];\n\tif (!Array.isArray(value)) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"${fieldPath}\": expected an array of tool ids.`,\n\t\t);\n\t}\n\tconst invalidEntry = value.find((entry) => typeof entry !== \"string\");\n\tif (invalidEntry !== undefined) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"${fieldPath}\": all entries must be strings.`,\n\t\t);\n\t}\n\treturn value as string[];\n}\n\nfunction assertPlacementConfig(\n\tvalue: unknown,\n): ToolPlacementConfig | undefined {\n\tif (value == null) return undefined;\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config at \"placement\": expected an object with section/item/passage arrays.',\n\t\t);\n\t}\n\treturn value as ToolPlacementConfig;\n}\n\nfunction assertPolicyConfig(value: unknown): ToolPolicyConfig | undefined {\n\tif (value == null) return undefined;\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config at \"policy\": expected an object with allowed/blocked arrays.',\n\t\t);\n\t}\n\treturn value as ToolPolicyConfig;\n}\n\nfunction assertProviderConfig(\n\tproviderId: string,\n\tvalue: unknown,\n): ToolProviderConfig | TextToSpeechToolProviderConfig | undefined {\n\tif (value == null) return undefined;\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}\": expected an object.`,\n\t\t);\n\t}\n\tconst config = value as ToolProviderConfig;\n\tconst isTTSRuntimeProviderId = (provider: unknown): boolean =>\n\t\tprovider === \"polly\" || provider === \"google\" || provider === \"custom\";\n\tif (\n\t\t\"enabled\" in config &&\n\t\tconfig.enabled !== undefined &&\n\t\ttypeof config.enabled !== \"boolean\"\n\t) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}.enabled\": expected a boolean.`,\n\t\t);\n\t}\n\tif (\n\t\t\"settings\" in config &&\n\t\tconfig.settings !== undefined &&\n\t\t!isPlainObject(config.settings)\n\t) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}.settings\": expected an object.`,\n\t\t);\n\t}\n\tif (\n\t\t\"provider\" in config &&\n\t\tconfig.provider !== undefined &&\n\t\t!isPlainObject(config.provider) &&\n\t\t!(providerId === \"textToSpeech\" && isTTSRuntimeProviderId(config.provider))\n\t) {\n\t\tthrow new Error(\n\t\t\t`Invalid tools config at \"providers.${providerId}.provider\": expected an object.`,\n\t\t);\n\t}\n\treturn config;\n}\n\nfunction assertProvidersConfig(value: unknown): ToolProvidersConfig {\n\tif (value == null) return {};\n\tif (!isPlainObject(value)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config at \"providers\": expected an object keyed by tool id.',\n\t\t);\n\t}\n\tconst normalized: ToolProvidersConfig = {};\n\tfor (const [providerId, providerConfig] of Object.entries(value)) {\n\t\tnormalized[providerId] = assertProviderConfig(providerId, providerConfig);\n\t}\n\treturn normalized;\n}\n\nexport function normalizeToolAlias(toolId: string): string {\n\tconst trimmed = toolId.trim();\n\tif (!trimmed) return \"\";\n\treturn trimmed;\n}\n\nexport function normalizeToolList(\n\ttoolIds: string[] | undefined | null,\n): string[] {\n\tif (!toolIds || toolIds.length === 0) return [];\n\tconst deduped = new Set<string>();\n\tfor (const rawToolId of toolIds) {\n\t\tif (typeof rawToolId !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t\"Invalid tools config: tool list entries must be strings.\",\n\t\t\t);\n\t\t}\n\t\tconst normalized = normalizeToolAlias(rawToolId);\n\t\tif (!normalized) continue;\n\t\tdeduped.add(normalized);\n\t}\n\treturn Array.from(deduped);\n}\n\nexport function parseToolList(input: string | undefined | null): string[] {\n\tif (!input) return [];\n\treturn normalizeToolList(\n\t\tinput\n\t\t\t.split(\",\")\n\t\t\t.map((entry) => entry.trim())\n\t\t\t.filter(Boolean),\n\t);\n}\n\nfunction assertPnpEnforcement(\n\tvalue: unknown,\n\tfieldPath: \"pnpEnforcement\",\n): ToolsPnpEnforcement | undefined {\n\tif (value == null) return undefined;\n\tif (value === \"on\" || value === \"off\") return value;\n\tthrow new Error(\n\t\t`Invalid tools config at \"${fieldPath}\": expected \"on\" or \"off\", got ${JSON.stringify(value)}.`,\n\t);\n}\n\nexport function normalizeToolsConfig(\n\tinput?: Partial<CanonicalToolsConfig> | null,\n): CanonicalToolsConfig {\n\tif (input != null && !isPlainObject(input)) {\n\t\tthrow new Error(\n\t\t\t'Invalid tools config: expected an object with \"policy\", \"placement\", and \"providers\".',\n\t\t);\n\t}\n\tconst policy = assertPolicyConfig(input?.policy);\n\tconst placement = assertPlacementConfig(input?.placement);\n\tconst providers = assertProvidersConfig(input?.providers);\n\tconst pnpEnforcement = assertPnpEnforcement(\n\t\tinput?.pnpEnforcement,\n\t\t\"pnpEnforcement\",\n\t);\n\n\tconst config: CanonicalToolsConfig = {\n\t\tpolicy: {\n\t\t\tallowed: normalizeToolList(\n\t\t\t\tassertStringArray(policy?.allowed, \"policy.allowed\"),\n\t\t\t),\n\t\t\tblocked: normalizeToolList(\n\t\t\t\tassertStringArray(policy?.blocked, \"policy.blocked\"),\n\t\t\t),\n\t\t},\n\t\tplacement: {\n\t\t\tsection: normalizeToolList(\n\t\t\t\tassertStringArray(placement?.section, \"placement.section\"),\n\t\t\t).length\n\t\t\t\t? normalizeToolList(\n\t\t\t\t\t\tassertStringArray(placement?.section, \"placement.section\"),\n\t\t\t\t\t)\n\t\t\t\t: [...DEFAULT_PLACEMENT.section],\n\t\t\titem: normalizeToolList(\n\t\t\t\tassertStringArray(placement?.item, \"placement.item\"),\n\t\t\t).length\n\t\t\t\t? normalizeToolList(\n\t\t\t\t\t\tassertStringArray(placement?.item, \"placement.item\"),\n\t\t\t\t\t)\n\t\t\t\t: [...DEFAULT_PLACEMENT.item],\n\t\t\tpassage: normalizeToolList(\n\t\t\t\tassertStringArray(placement?.passage, \"placement.passage\"),\n\t\t\t).length\n\t\t\t\t? normalizeToolList(\n\t\t\t\t\t\tassertStringArray(placement?.passage, \"placement.passage\"),\n\t\t\t\t\t)\n\t\t\t\t: [...DEFAULT_PLACEMENT.passage],\n\t\t},\n\t\tproviders,\n\t};\n\tif (pnpEnforcement) {\n\t\tconfig.pnpEnforcement = pnpEnforcement;\n\t}\n\treturn config;\n}\n"]}
@@ -6,7 +6,43 @@
6
6
  *
7
7
  * Part of PIE Assessment Toolkit.
8
8
  */
9
- import { segmentSentences as segmentTextToSentences, } from "./text-segmentation.js";
9
+ import { segmentSentences as segmentTextToSentences } from "./text-segmentation.js";
10
+ const normalizeLanguageCode = (value) => String(value || "")
11
+ .trim()
12
+ .toLowerCase();
13
+ const browserLanguage = () => {
14
+ const navigatorLanguage = typeof navigator !== "undefined"
15
+ ? navigator.language || navigator.languages?.[0]
16
+ : "";
17
+ return normalizeLanguageCode(navigatorLanguage || "en-US");
18
+ };
19
+ const findBrowserVoice = (voices, preferredName) => {
20
+ if (preferredName) {
21
+ const explicit = voices.find((voice) => voice.name === preferredName);
22
+ if (explicit)
23
+ return explicit;
24
+ }
25
+ const language = browserLanguage();
26
+ const languagePrefix = language.split("-")[0] || "en";
27
+ const matchesLanguage = (voice) => {
28
+ const voiceLanguage = normalizeLanguageCode(voice.lang);
29
+ return voiceLanguage === language || voiceLanguage.startsWith(`${languagePrefix}-`);
30
+ };
31
+ const ranked = [
32
+ (voice) => voice.localService && matchesLanguage(voice),
33
+ (voice) => voice.default && matchesLanguage(voice),
34
+ (voice) => matchesLanguage(voice),
35
+ (voice) => voice.localService,
36
+ (voice) => voice.default,
37
+ ];
38
+ for (const predicate of ranked) {
39
+ const voice = voices.find(predicate);
40
+ if (voice)
41
+ return voice;
42
+ }
43
+ return voices[0] || null;
44
+ };
45
+ const shouldAssignBrowserVoice = (voice) => !voice.default;
10
46
  /**
11
47
  * Browser TTS Provider
12
48
  *
@@ -183,25 +219,29 @@ class BrowserTTSProviderImpl {
183
219
  resolve(false);
184
220
  return;
185
221
  }
186
- this.utterance = new SpeechSynthesisUtterance(chunkText);
222
+ const utterance = new SpeechSynthesisUtterance(chunkText);
223
+ this.utterance = utterance;
187
224
  // Apply config
188
- if (this.config?.voice) {
189
- const voices = speechSynthesis.getVoices();
190
- const voice = voices.find((v) => v.name === this.config.voice);
191
- if (voice)
192
- this.utterance.voice = voice;
193
- }
225
+ const voice = findBrowserVoice(speechSynthesis.getVoices(), this.config?.voice);
226
+ if (voice && shouldAssignBrowserVoice(voice))
227
+ utterance.voice = voice;
194
228
  if (this.config?.rate)
195
- this.utterance.rate = this.config.rate;
229
+ utterance.rate = this.config.rate;
196
230
  if (this.config?.pitch)
197
- this.utterance.pitch = this.config.pitch;
198
- this.utterance.onstart = () => {
231
+ utterance.pitch = this.config.pitch;
232
+ const clearOwnedUtterance = () => {
233
+ if (this.utterance === utterance) {
234
+ this.utterance = null;
235
+ }
236
+ };
237
+ utterance.onstart = () => {
199
238
  if (runId !== this.speakRunId)
200
239
  return;
201
240
  this._isPlaying = true;
202
241
  this._isPaused = false;
203
242
  };
204
- this.utterance.onend = () => {
243
+ utterance.onend = () => {
244
+ clearOwnedUtterance();
205
245
  if (runId !== this.speakRunId) {
206
246
  resolve(false);
207
247
  return;
@@ -210,7 +250,8 @@ class BrowserTTSProviderImpl {
210
250
  this._isPaused = false;
211
251
  resolve(true);
212
252
  };
213
- this.utterance.onerror = (event) => {
253
+ utterance.onerror = (event) => {
254
+ clearOwnedUtterance();
214
255
  if (runId !== this.speakRunId) {
215
256
  resolve(false);
216
257
  return;
@@ -223,17 +264,17 @@ class BrowserTTSProviderImpl {
223
264
  }
224
265
  reject(new Error(`Speech synthesis error: ${event.error}`));
225
266
  };
226
- this.utterance.onpause = () => {
267
+ utterance.onpause = () => {
227
268
  if (runId !== this.speakRunId)
228
269
  return;
229
270
  this._isPaused = true;
230
271
  };
231
- this.utterance.onresume = () => {
272
+ utterance.onresume = () => {
232
273
  if (runId !== this.speakRunId)
233
274
  return;
234
275
  this._isPaused = false;
235
276
  };
236
- this.utterance.onboundary = (event) => {
277
+ utterance.onboundary = (event) => {
237
278
  if (runId !== this.speakRunId)
238
279
  return;
239
280
  console.log("[BrowserProvider] Boundary event:", event.name, "charIndex:", event.charIndex, "charLength:", event.charLength);
@@ -258,7 +299,7 @@ class BrowserTTSProviderImpl {
258
299
  console.log("[BrowserProvider] Calling onWordBoundary with word:", word, "at position:", absoluteBoundaryStart);
259
300
  this.onWordBoundary(word, absoluteBoundaryStart, wordLength);
260
301
  };
261
- speechSynthesis.speak(this.utterance);
302
+ speechSynthesis.speak(utterance);
262
303
  });
263
304
  }
264
305
  pause() {
@@ -273,9 +314,10 @@ class BrowserTTSProviderImpl {
273
314
  }
274
315
  stop() {
275
316
  this.speakRunId += 1;
276
- if (this._isPlaying) {
317
+ if (this._isPlaying || this.utterance) {
277
318
  speechSynthesis.cancel();
278
319
  }
320
+ this.utterance = null;
279
321
  this._isPlaying = false;
280
322
  this._isPaused = false;
281
323
  }
@@ -1 +1 @@
1
- {"version":3,"file":"browser-provider.js","sourceRoot":"","sources":["../../../src/services/tts/browser-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,OAAO,EACN,gBAAgB,IAAI,sBAAsB,GAC1C,MAAM,wBAAwB,CAAC;AAQhC;;;;GAIG;AACH,MAAM,OAAO,kBAAkB;IACrB,UAAU,GAAG,SAAS,CAAC;IACvB,YAAY,GAAG,0BAA0B,CAAC;IAC1C,OAAO,GAAG,OAAO,CAAC;IAE3B,KAAK,CAAC,UAAU,CAAC,MAAiB;QACjC,YAAY;QACZ,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACpE,CAAC;QAED,wBAAwB;QACxB,IAAI,CAAC,CAAC,iBAAiB,IAAI,MAAM,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QAClE,CAAC;QAED,OAAO,IAAI,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;IAED,eAAe,CAAC,OAAmB;QAClC,oCAAoC;QACpC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,eAAe;QACd,OAAO;YACN,aAAa,EAAE,IAAI;YACnB,cAAc,EAAE,IAAI;YACpB,oBAAoB,EAAE,IAAI;YAC1B,sBAAsB,EAAE,IAAI;YAC5B,mBAAmB,EAAE,IAAI;YACzB,oBAAoB,EAAE,IAAI;YAC1B,uEAAuE;YACvE,YAAY,EAAE,KAAK;SACnB,CAAC;IACH,CAAC;IAED,OAAO;QACN,yCAAyC;IAC1C,CAAC;CACD;AAED;;;;GAIG;AACH,MAAM,sBAAsB;IACnB,SAAS,GAAoC,IAAI,CAAC;IAClD,MAAM,GAAqB,IAAI,CAAC;IAChC,UAAU,GAAG,KAAK,CAAC;IACnB,SAAS,GAAG,KAAK,CAAC;IAClB,UAAU,GAAG,CAAC,CAAC;IAEvB,YAAY,MAAiB;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY;QACvB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACxC,CAAC;QACD,6DAA6D;QAC7D,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC;QAE9B,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC5B,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC/B,MAAM;YACP,CAAC;YACD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YAC9E,IAAI,CAAC,cAAc,EAAE,CAAC;gBACrB,MAAM;YACP,CAAC;QACF,CAAC;IACF,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAA4B;QAC/C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC;QAC9B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU;gBAAE,MAAM;YACrC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAClD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC5B,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU;oBAAE,MAAM;gBACrC,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,UAAU,CAC3C,KAAK,CAAC,IAAI,EACV,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,MAAM,EAClC,KAAK,CACL,CAAC;gBACF,IAAI,CAAC,cAAc;oBAAE,MAAM;YAC5B,CAAC;YACD,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC;YACpE,IAAI,YAAY,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;gBACnD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;gBACpE,IAAI,CAAC,cAAc;oBAAE,MAAM;YAC5B,CAAC;QACF,CAAC;IACF,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,OAAe,EAAE,KAAa;QACxD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC7D,OAAO,KAAK,KAAK,IAAI,CAAC,UAAU,CAAC;IAClC,CAAC;IAEO,eAAe,CAAC,IAAY;QACnC,MAAM,gBAAgB,GAAG,GAAG,CAAC;QAC7B,IAAI,IAAI,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC;YACrC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9B,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9C,MAAM,MAAM,GAA4C,EAAE,CAAC;QAC3D,IAAI,WAAW,GAAG,EAAE,CAAC;QACrB,IAAI,aAAa,GAAG,CAAC,CAAC;QAEtB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,OAAO;gBAAE,SAAS;YACvB,MAAM,kBAAkB,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACtD,MAAM,YAAY,GACjB,QAAQ,CAAC,MAAM,GAAG,CAAC,kBAAkB,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;YAExE,IAAI,CAAC,WAAW,EAAE,CAAC;gBAClB,WAAW,GAAG,OAAO,CAAC;gBACtB,aAAa,GAAG,YAAY,CAAC;gBAC7B,SAAS;YACV,CAAC;YAED,MAAM,SAAS,GAAG,GAAG,WAAW,IAAI,OAAO,EAAE,CAAC;YAC9C,IAAI,SAAS,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC;gBAC1C,WAAW,GAAG,SAAS,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACP,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;gBAC1D,WAAW,GAAG,OAAO,CAAC;gBACtB,aAAa,GAAG,YAAY,CAAC;YAC9B,CAAC;QACF,CAAC;QAED,IAAI,WAAW,EAAE,CAAC;YACjB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IACvD,CAAC;IAEO,gBAAgB;QACvB,MAAM,eAAe,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,IAAI,EAAE,CAG1D,CAAC;QACF,OAAO,eAAe,CAAC,aAAa,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC;IACvE,CAAC;IAEO,qBAAqB;QAK5B,MAAM,eAAe,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,IAAI,EAAE,CAG1D,CAAC;QACF,MAAM,SAAS,GAAG,CAAC,eAAe,CAAC,SAAS,IAAI,EAAE,CAA4B,CAAC;QAC/E,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC;QAC5B,MAAM,YAAY,GAAG,IAAI,KAAK,WAAW,CAAC;QAC1C,MAAM,MAAM,GACX,OAAO,SAAS,CAAC,MAAM,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YACzE,CAAC,CAAC,SAAS,CAAC,MAAM;YAClB,CAAC,CAAC,OAAO,eAAe,CAAC,MAAM,KAAK,QAAQ;gBACzC,eAAe,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;gBAC1C,CAAC,CAAC,eAAe,CAAC,MAAM;gBACxB,CAAC,CAAC,SAAS,CAAC;QACf,OAAO;YACN,oBAAoB,EAAE,YAAY;YAClC,gBAAgB,EAAE,YAAY;YAC9B,MAAM;SACN,CAAC;IACH,CAAC;IAEO,gBAAgB,CAAC,IAAY;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC5C,OAAO,sBAAsB,CAAC,IAAI,EAAE;YACnC,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,oBAAoB,EAAE,MAAM,CAAC,oBAAoB;SACjD,CAAC,CAAC;IACJ,CAAC;IAEO,eAAe,CAAC,IAAY,EAAE,KAAa;QAClD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QAC1C,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;IAChC,CAAC;IAEO,KAAK,CAAC,UAAU,CACvB,SAAiB,EACjB,WAAmB,EACnB,KAAa;QAEb,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACtC,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC/B,OAAO,CAAC,KAAK,CAAC,CAAC;gBACf,OAAO;YACR,CAAC;YACD,IAAI,CAAC,SAAS,GAAG,IAAI,wBAAwB,CAAC,SAAS,CAAC,CAAC;YAEzD,eAAe;YACf,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;gBACxB,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,EAAE,CAAC;gBAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,MAAO,CAAC,KAAK,CAAC,CAAC;gBAChE,IAAI,KAAK;oBAAE,IAAI,CAAC,SAAU,CAAC,KAAK,GAAG,KAAK,CAAC;YAC1C,CAAC;YAED,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI;gBAAE,IAAI,CAAC,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAC9D,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK;gBAAE,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YAEjE,IAAI,CAAC,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;gBAC7B,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU;oBAAE,OAAO;gBACtC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;gBACvB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACxB,CAAC,CAAC;YAEF,IAAI,CAAC,SAAS,CAAC,KAAK,GAAG,GAAG,EAAE;gBAC3B,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;oBAC/B,OAAO,CAAC,KAAK,CAAC,CAAC;oBACf,OAAO;gBACR,CAAC;gBACD,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;gBACxB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;gBACvB,OAAO,CAAC,IAAI,CAAC,CAAC;YACf,CAAC,CAAC;YAEF,IAAI,CAAC,SAAS,CAAC,OAAO,GAAG,CAAC,KAAK,EAAE,EAAE;gBAClC,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;oBAC/B,OAAO,CAAC,KAAK,CAAC,CAAC;oBACf,OAAO;gBACR,CAAC;gBACD,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;gBACxB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;gBACvB,IAAI,KAAK,CAAC,KAAK,KAAK,aAAa,IAAI,KAAK,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;oBACjE,OAAO,CAAC,KAAK,CAAC,CAAC;oBACf,OAAO;gBACR,CAAC;gBACD,MAAM,CAAC,IAAI,KAAK,CAAC,2BAA2B,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAC7D,CAAC,CAAC;YAEF,IAAI,CAAC,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;gBAC7B,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU;oBAAE,OAAO;gBACtC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACvB,CAAC,CAAC;YAEF,IAAI,CAAC,SAAS,CAAC,QAAQ,GAAG,GAAG,EAAE;gBAC9B,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU;oBAAE,OAAO;gBACtC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACxB,CAAC,CAAC;YAEF,IAAI,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,KAAK,EAAE,EAAE;gBACrC,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU;oBAAE,OAAO;gBACtC,OAAO,CAAC,GAAG,CACV,mCAAmC,EACnC,KAAK,CAAC,IAAI,EACV,YAAY,EACZ,KAAK,CAAC,SAAS,EACf,aAAa,EACb,KAAK,CAAC,UAAU,CAChB,CAAC;gBACF,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc;oBAAE,OAAO;gBAC1D,IAAI,IAAI,CAAC,gBAAgB,EAAE,KAAK,UAAU,EAAE,CAAC;oBAC5C,OAAO;gBACR,CAAC;gBAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CACzB,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAC5D,CAAC;gBACF,MAAM,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;gBACrD,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBAClE,MAAM,UAAU,GACf,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC;oBAC/B,cAAc,GAAG,CAAC;oBAClB,cAAc,IAAI,EAAE;oBACpB,SAAS,GAAG,cAAc,IAAI,SAAS,CAAC,MAAM;oBAC7C,CAAC,CAAC,cAAc;oBAChB,CAAC,CAAC,cAAc,CAAC;gBACnB,MAAM,IAAI,GAAG,SAAS;qBACpB,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC;qBACxE,IAAI,EAAE,CAAC;gBACT,MAAM,qBAAqB,GAAG,WAAW,GAAG,SAAS,CAAC;gBACtD,OAAO,CAAC,GAAG,CACV,qDAAqD,EACrD,IAAI,EACJ,cAAc,EACd,qBAAqB,CACrB,CAAC;gBACF,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,qBAAqB,EAAE,UAAU,CAAC,CAAC;YAC9D,CAAC,CAAC;YAEF,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACJ,CAAC;IAED,KAAK;QACJ,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACxC,eAAe,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC;IACF,CAAC;IAED,MAAM;QACL,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACvC,eAAe,CAAC,MAAM,EAAE,CAAC;QAC1B,CAAC;IACF,CAAC;IAED,IAAI;QACH,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACrB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,eAAe,CAAC,MAAM,EAAE,CAAC;QAC1B,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACxB,CAAC;IAED,SAAS;QACR,OAAO,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;IAC3C,CAAC;IAED,QAAQ;QACP,OAAO,IAAI,CAAC,SAAS,CAAC;IACvB,CAAC;IAED;;;OAGG;IACH,cAAc,CAAC,QAA4B;QAC1C,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClB,IAAI,CAAC,MAAM,GAAG,EAAe,CAAC;QAC/B,CAAC;QAED,kCAAkC;QAClC,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;QAClC,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;QACpC,CAAC;QACD,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;QACpC,CAAC;QACD,IAAI,QAAQ,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;YAC5C,IAAI,CAAC,MAAM,CAAC,eAAe,GAAG;gBAC7B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,IAAI,EAAE,CAAC;gBACtC,GAAG,CAAC,QAAQ,CAAC,eAAe,IAAI,EAAE,CAAC;aACnC,CAAC;QACH,CAAC;IACF,CAAC;IAED,cAAc,CAA6D;CAC3E","sourcesContent":["/**\n * Browser TTS Provider\n *\n * Text-to-speech provider using the browser's Web Speech API.\n * Works in all modern browsers that support SpeechSynthesis.\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type {\n\tITTSProvider,\n\tITTSProviderImplementation,\n\tTTSConfig,\n\tTTSFeature,\n\tTTSProviderCapabilities,\n} from \"@pie-players/pie-tts\";\nimport {\n\tsegmentSentences as segmentTextToSentences,\n} from \"./text-segmentation.js\";\n\ninterface TTSSpeechSegment {\n\ttext: string;\n\tstartOffset: number;\n\tpauseMsAfter?: number;\n}\n\n/**\n * Browser TTS Provider\n *\n * Stateless factory for creating browser-based TTS implementations.\n */\nexport class BrowserTTSProvider implements ITTSProvider {\n\treadonly providerId = \"browser\";\n\treadonly providerName = \"Browser Speech Synthesis\";\n\treadonly version = \"1.0.0\";\n\n\tasync initialize(config: TTSConfig): Promise<ITTSProviderImplementation> {\n\t\t// SSR guard\n\t\tif (typeof window === \"undefined\") {\n\t\t\tthrow new Error(\"BrowserTTSProvider requires browser environment\");\n\t\t}\n\n\t\t// Check browser support\n\t\tif (!(\"speechSynthesis\" in window)) {\n\t\t\tthrow new Error(\"Browser does not support Speech Synthesis API\");\n\t\t}\n\n\t\treturn new BrowserTTSProviderImpl(config);\n\t}\n\n\tsupportsFeature(feature: TTSFeature): boolean {\n\t\t// Browser supports all TTS features\n\t\treturn true;\n\t}\n\n\tgetCapabilities(): TTSProviderCapabilities {\n\t\treturn {\n\t\t\tsupportsPause: true,\n\t\t\tsupportsResume: true,\n\t\t\tsupportsWordBoundary: true,\n\t\t\tsupportsVoiceSelection: true,\n\t\t\tsupportsRateControl: true,\n\t\t\tsupportsPitchControl: true,\n\t\t\t// The Web Speech API voices plain text only; SSML tags are read aloud.\n\t\t\tsupportsSSML: false,\n\t\t};\n\t}\n\n\tdestroy(): void {\n\t\t// No cleanup needed for browser provider\n\t}\n}\n\n/**\n * Browser TTS Provider Implementation\n *\n * Handles actual speech synthesis using the Web Speech API.\n */\nclass BrowserTTSProviderImpl implements ITTSProviderImplementation {\n\tprivate utterance: SpeechSynthesisUtterance | null = null;\n\tprivate config: TTSConfig | null = null;\n\tprivate _isPlaying = false;\n\tprivate _isPaused = false;\n\tprivate speakRunId = 0;\n\n\tconstructor(config: TTSConfig) {\n\t\tthis.config = config;\n\t}\n\n\tasync speak(text: string): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\t// Invalidate any in-flight run and cancel current utterance.\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\n\t\tconst chunks = this.splitIntoChunks(text);\n\t\tfor (const chunk of chunks) {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId);\n\t\t\tif (!shouldContinue) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync speakSegments(segments: TTSSpeechSegment[]): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\t\tfor (const segment of segments) {\n\t\t\tif (runId !== this.speakRunId) break;\n\t\t\tconst chunks = this.splitIntoChunks(segment.text);\n\t\t\tfor (const chunk of chunks) {\n\t\t\t\tif (runId !== this.speakRunId) break;\n\t\t\t\tconst shouldContinue = await this.speakChunk(\n\t\t\t\t\tchunk.text,\n\t\t\t\t\tsegment.startOffset + chunk.offset,\n\t\t\t\t\trunId,\n\t\t\t\t);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t\tconst pauseMsAfter = Math.max(0, Number(segment.pauseMsAfter || 0));\n\t\t\tif (pauseMsAfter > 0 && runId === this.speakRunId) {\n\t\t\t\tconst shouldContinue = await this.waitForPause(pauseMsAfter, runId);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async waitForPause(pauseMs: number, runId: number): Promise<boolean> {\n\t\tawait new Promise((resolve) => setTimeout(resolve, pauseMs));\n\t\treturn runId === this.speakRunId;\n\t}\n\n\tprivate splitIntoChunks(text: string): Array<{ text: string; offset: number }> {\n\t\tconst MAX_CHUNK_LENGTH = 260;\n\t\tif (text.length <= MAX_CHUNK_LENGTH) {\n\t\t\treturn [{ text, offset: 0 }];\n\t\t}\n\n\t\tconst sentences = this.segmentSentences(text);\n\t\tconst chunks: Array<{ text: string; offset: number }> = [];\n\t\tlet currentText = \"\";\n\t\tlet currentOffset = 0;\n\n\t\tfor (const sentence of sentences) {\n\t\t\tconst trimmed = sentence.text.trim();\n\t\t\tif (!trimmed) continue;\n\t\t\tconst firstNonWhitespace = sentence.text.search(/\\S/);\n\t\t\tconst trimmedStart =\n\t\t\t\tsentence.offset + (firstNonWhitespace === -1 ? 0 : firstNonWhitespace);\n\n\t\t\tif (!currentText) {\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst candidate = `${currentText} ${trimmed}`;\n\t\t\tif (candidate.length <= MAX_CHUNK_LENGTH) {\n\t\t\t\tcurrentText = candidate;\n\t\t\t} else {\n\t\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t}\n\t\t}\n\n\t\tif (currentText) {\n\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t}\n\t\treturn chunks.length ? chunks : [{ text, offset: 0 }];\n\t}\n\n\tprivate getHighlightMode(): \"word\" | \"sentence\" {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\treturn providerOptions.highlightMode === \"word\" ? \"word\" : \"sentence\";\n\t}\n\n\tprivate getSegmentationPolicy(): {\n\t\tuseSentenceSegmenter: boolean;\n\t\tuseWordSegmenter: boolean;\n\t\tlocale?: string;\n\t} {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\tconst segmenter = (providerOptions.segmenter || {}) as Record<string, unknown>;\n\t\tconst mode = segmenter.mode;\n\t\tconst useSegmenter = mode !== \"regexOnly\";\n\t\tconst locale =\n\t\t\ttypeof segmenter.locale === \"string\" && segmenter.locale.trim().length > 0\n\t\t\t\t? segmenter.locale\n\t\t\t\t: typeof providerOptions.locale === \"string\" &&\n\t\t\t\t\t providerOptions.locale.trim().length > 0\n\t\t\t\t\t? providerOptions.locale\n\t\t\t\t\t: undefined;\n\t\treturn {\n\t\t\tuseSentenceSegmenter: useSegmenter,\n\t\t\tuseWordSegmenter: useSegmenter,\n\t\t\tlocale,\n\t\t};\n\t}\n\n\tprivate segmentSentences(text: string): Array<{ text: string; offset: number }> {\n\t\tconst policy = this.getSegmentationPolicy();\n\t\treturn segmentTextToSentences(text, {\n\t\t\tlocale: policy.locale,\n\t\t\tuseSentenceSegmenter: policy.useSentenceSegmenter,\n\t\t});\n\t}\n\n\tprivate inferWordLength(text: string, index: number): number {\n\t\tconst safeIndex = Math.max(0, Math.min(index, Math.max(0, text.length - 1)));\n\t\tconst slice = text.slice(safeIndex);\n\t\tconst match = slice.match(/^\\s*([^\\s]+)/);\n\t\treturn match?.[1]?.length || 1;\n\t}\n\n\tprivate async speakChunk(\n\t\tchunkText: string,\n\t\tchunkOffset: number,\n\t\trunId: number,\n\t): Promise<boolean> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tresolve(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.utterance = new SpeechSynthesisUtterance(chunkText);\n\n\t\t\t// Apply config\n\t\t\tif (this.config?.voice) {\n\t\t\t\tconst voices = speechSynthesis.getVoices();\n\t\t\t\tconst voice = voices.find((v) => v.name === this.config!.voice);\n\t\t\t\tif (voice) this.utterance!.voice = voice;\n\t\t\t}\n\n\t\t\tif (this.config?.rate) this.utterance.rate = this.config.rate;\n\t\t\tif (this.config?.pitch) this.utterance.pitch = this.config.pitch;\n\n\t\t\tthis.utterance.onstart = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPlaying = true;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tthis.utterance.onend = () => {\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tresolve(true);\n\t\t\t};\n\n\t\t\tthis.utterance.onerror = (event) => {\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tif (event.error === \"interrupted\" || event.error === \"canceled\") {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treject(new Error(`Speech synthesis error: ${event.error}`));\n\t\t\t};\n\n\t\t\tthis.utterance.onpause = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = true;\n\t\t\t};\n\n\t\t\tthis.utterance.onresume = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tthis.utterance.onboundary = (event) => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Boundary event:\",\n\t\t\t\t\tevent.name,\n\t\t\t\t\t\"charIndex:\",\n\t\t\t\t\tevent.charIndex,\n\t\t\t\t\t\"charLength:\",\n\t\t\t\t\tevent.charLength,\n\t\t\t\t);\n\t\t\t\tif (event.name !== \"word\" || !this.onWordBoundary) return;\n\t\t\t\tif (this.getHighlightMode() === \"sentence\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst charIndex = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tMath.min(event.charIndex, Math.max(0, chunkText.length - 1)),\n\t\t\t\t);\n\t\t\t\tconst reportedLength = Number(event.charLength || 0);\n\t\t\t\tconst inferredLength = this.inferWordLength(chunkText, charIndex);\n\t\t\t\tconst wordLength =\n\t\t\t\t\tNumber.isFinite(reportedLength) &&\n\t\t\t\t\treportedLength > 0 &&\n\t\t\t\t\treportedLength <= 80 &&\n\t\t\t\t\tcharIndex + reportedLength <= chunkText.length\n\t\t\t\t\t\t? reportedLength\n\t\t\t\t\t\t: inferredLength;\n\t\t\t\tconst word = chunkText\n\t\t\t\t\t.substring(charIndex, Math.min(chunkText.length, charIndex + wordLength))\n\t\t\t\t\t.trim();\n\t\t\t\tconst absoluteBoundaryStart = chunkOffset + charIndex;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Calling onWordBoundary with word:\",\n\t\t\t\t\tword,\n\t\t\t\t\t\"at position:\",\n\t\t\t\t\tabsoluteBoundaryStart,\n\t\t\t\t);\n\t\t\t\tthis.onWordBoundary(word, absoluteBoundaryStart, wordLength);\n\t\t\t};\n\n\t\t\tspeechSynthesis.speak(this.utterance);\n\t\t});\n\t}\n\n\tpause(): void {\n\t\tif (this._isPlaying && !this._isPaused) {\n\t\t\tspeechSynthesis.pause();\n\t\t}\n\t}\n\n\tresume(): void {\n\t\tif (this._isPlaying && this._isPaused) {\n\t\t\tspeechSynthesis.resume();\n\t\t}\n\t}\n\n\tstop(): void {\n\t\tthis.speakRunId += 1;\n\t\tif (this._isPlaying) {\n\t\t\tspeechSynthesis.cancel();\n\t\t}\n\t\tthis._isPlaying = false;\n\t\tthis._isPaused = false;\n\t}\n\n\tisPlaying(): boolean {\n\t\treturn this._isPlaying && !this._isPaused;\n\t}\n\n\tisPaused(): boolean {\n\t\treturn this._isPaused;\n\t}\n\n\t/**\n\t * Update settings dynamically (rate, pitch, voice)\n\t * Changes take effect on the next speak() call\n\t */\n\tupdateSettings(settings: Partial<TTSConfig>): void {\n\t\tif (!this.config) {\n\t\t\tthis.config = {} as TTSConfig;\n\t\t}\n\n\t\t// Update config with new settings\n\t\tif (settings.rate !== undefined) {\n\t\t\tthis.config.rate = settings.rate;\n\t\t}\n\t\tif (settings.pitch !== undefined) {\n\t\t\tthis.config.pitch = settings.pitch;\n\t\t}\n\t\tif (settings.voice !== undefined) {\n\t\t\tthis.config.voice = settings.voice;\n\t\t}\n\t\tif (settings.providerOptions !== undefined) {\n\t\t\tthis.config.providerOptions = {\n\t\t\t\t...(this.config.providerOptions || {}),\n\t\t\t\t...(settings.providerOptions || {}),\n\t\t\t};\n\t\t}\n\t}\n\n\tonWordBoundary?: (word: string, position: number, length?: number) => void;\n}\n"]}
1
+ {"version":3,"file":"browser-provider.js","sourceRoot":"","sources":["../../../src/services/tts/browser-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Browser TTS Provider\n *\n * Text-to-speech provider using the browser's Web Speech API.\n * Works in all modern browsers that support SpeechSynthesis.\n *\n * Part of PIE Assessment Toolkit.\n */\n\nimport type {\n\tITTSProvider,\n\tITTSProviderImplementation,\n\tTTSConfig,\n\tTTSFeature,\n\tTTSProviderCapabilities,\n} from \"@pie-players/pie-tts\";\nimport { segmentSentences as segmentTextToSentences } from \"./text-segmentation.js\";\n\ninterface TTSSpeechSegment {\n\ttext: string;\n\tstartOffset: number;\n\tpauseMsAfter?: number;\n}\n\nconst normalizeLanguageCode = (value: unknown): string =>\n\tString(value || \"\")\n\t\t.trim()\n\t\t.toLowerCase();\n\nconst browserLanguage = (): string => {\n\tconst navigatorLanguage =\n\t\ttypeof navigator !== \"undefined\"\n\t\t\t? navigator.language || navigator.languages?.[0]\n\t\t\t: \"\";\n\treturn normalizeLanguageCode(navigatorLanguage || \"en-US\");\n};\n\nconst findBrowserVoice = (\n\tvoices: SpeechSynthesisVoice[],\n\tpreferredName?: string,\n): SpeechSynthesisVoice | null => {\n\tif (preferredName) {\n\t\tconst explicit = voices.find((voice) => voice.name === preferredName);\n\t\tif (explicit) return explicit;\n\t}\n\tconst language = browserLanguage();\n\tconst languagePrefix = language.split(\"-\")[0] || \"en\";\n\tconst matchesLanguage = (voice: SpeechSynthesisVoice) => {\n\t\tconst voiceLanguage = normalizeLanguageCode(voice.lang);\n\t\treturn voiceLanguage === language || voiceLanguage.startsWith(`${languagePrefix}-`);\n\t};\n\tconst ranked = [\n\t\t(voice: SpeechSynthesisVoice) => voice.localService && matchesLanguage(voice),\n\t\t(voice: SpeechSynthesisVoice) => voice.default && matchesLanguage(voice),\n\t\t(voice: SpeechSynthesisVoice) => matchesLanguage(voice),\n\t\t(voice: SpeechSynthesisVoice) => voice.localService,\n\t\t(voice: SpeechSynthesisVoice) => voice.default,\n\t];\n\tfor (const predicate of ranked) {\n\t\tconst voice = voices.find(predicate);\n\t\tif (voice) return voice;\n\t}\n\treturn voices[0] || null;\n};\n\nconst shouldAssignBrowserVoice = (voice: SpeechSynthesisVoice): boolean => !voice.default;\n\n/**\n * Browser TTS Provider\n *\n * Stateless factory for creating browser-based TTS implementations.\n */\nexport class BrowserTTSProvider implements ITTSProvider {\n\treadonly providerId = \"browser\";\n\treadonly providerName = \"Browser Speech Synthesis\";\n\treadonly version = \"1.0.0\";\n\n\tasync initialize(config: TTSConfig): Promise<ITTSProviderImplementation> {\n\t\t// SSR guard\n\t\tif (typeof window === \"undefined\") {\n\t\t\tthrow new Error(\"BrowserTTSProvider requires browser environment\");\n\t\t}\n\n\t\t// Check browser support\n\t\tif (!(\"speechSynthesis\" in window)) {\n\t\t\tthrow new Error(\"Browser does not support Speech Synthesis API\");\n\t\t}\n\n\t\treturn new BrowserTTSProviderImpl(config);\n\t}\n\n\tsupportsFeature(feature: TTSFeature): boolean {\n\t\t// Browser supports all TTS features\n\t\treturn true;\n\t}\n\n\tgetCapabilities(): TTSProviderCapabilities {\n\t\treturn {\n\t\t\tsupportsPause: true,\n\t\t\tsupportsResume: true,\n\t\t\tsupportsWordBoundary: true,\n\t\t\tsupportsVoiceSelection: true,\n\t\t\tsupportsRateControl: true,\n\t\t\tsupportsPitchControl: true,\n\t\t\t// The Web Speech API voices plain text only; SSML tags are read aloud.\n\t\t\tsupportsSSML: false,\n\t\t};\n\t}\n\n\tdestroy(): void {\n\t\t// No cleanup needed for browser provider\n\t}\n}\n\n/**\n * Browser TTS Provider Implementation\n *\n * Handles actual speech synthesis using the Web Speech API.\n */\nclass BrowserTTSProviderImpl implements ITTSProviderImplementation {\n\tprivate utterance: SpeechSynthesisUtterance | null = null;\n\tprivate config: TTSConfig | null = null;\n\tprivate _isPlaying = false;\n\tprivate _isPaused = false;\n\tprivate speakRunId = 0;\n\n\tconstructor(config: TTSConfig) {\n\t\tthis.config = config;\n\t}\n\n\tasync speak(text: string): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\t// Invalidate any in-flight run and cancel current utterance.\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\n\t\tconst chunks = this.splitIntoChunks(text);\n\t\tfor (const chunk of chunks) {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst shouldContinue = await this.speakChunk(\n\t\t\t\tchunk.text,\n\t\t\t\tchunk.offset,\n\t\t\t\trunId,\n\t\t\t);\n\t\t\tif (!shouldContinue) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tasync speakSegments(segments: TTSSpeechSegment[]): Promise<void> {\n\t\tif (!this.config) {\n\t\t\tthrow new Error(\"TTS not initialized\");\n\t\t}\n\t\tthis.stop();\n\t\tconst runId = this.speakRunId;\n\t\tfor (const segment of segments) {\n\t\t\tif (runId !== this.speakRunId) break;\n\t\t\tconst chunks = this.splitIntoChunks(segment.text);\n\t\t\tfor (const chunk of chunks) {\n\t\t\t\tif (runId !== this.speakRunId) break;\n\t\t\t\tconst shouldContinue = await this.speakChunk(\n\t\t\t\t\tchunk.text,\n\t\t\t\t\tsegment.startOffset + chunk.offset,\n\t\t\t\t\trunId,\n\t\t\t\t);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t\tconst pauseMsAfter = Math.max(0, Number(segment.pauseMsAfter || 0));\n\t\t\tif (pauseMsAfter > 0 && runId === this.speakRunId) {\n\t\t\t\tconst shouldContinue = await this.waitForPause(pauseMsAfter, runId);\n\t\t\t\tif (!shouldContinue) break;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async waitForPause(pauseMs: number, runId: number): Promise<boolean> {\n\t\tawait new Promise((resolve) => setTimeout(resolve, pauseMs));\n\t\treturn runId === this.speakRunId;\n\t}\n\n\tprivate splitIntoChunks(\n\t\ttext: string,\n\t): Array<{ text: string; offset: number }> {\n\t\tconst MAX_CHUNK_LENGTH = 260;\n\t\tif (text.length <= MAX_CHUNK_LENGTH) {\n\t\t\treturn [{ text, offset: 0 }];\n\t\t}\n\n\t\tconst sentences = this.segmentSentences(text);\n\t\tconst chunks: Array<{ text: string; offset: number }> = [];\n\t\tlet currentText = \"\";\n\t\tlet currentOffset = 0;\n\n\t\tfor (const sentence of sentences) {\n\t\t\tconst trimmed = sentence.text.trim();\n\t\t\tif (!trimmed) continue;\n\t\t\tconst firstNonWhitespace = sentence.text.search(/\\S/);\n\t\t\tconst trimmedStart =\n\t\t\t\tsentence.offset + (firstNonWhitespace === -1 ? 0 : firstNonWhitespace);\n\n\t\t\tif (!currentText) {\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst candidate = `${currentText} ${trimmed}`;\n\t\t\tif (candidate.length <= MAX_CHUNK_LENGTH) {\n\t\t\t\tcurrentText = candidate;\n\t\t\t} else {\n\t\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t\t\tcurrentText = trimmed;\n\t\t\t\tcurrentOffset = trimmedStart;\n\t\t\t}\n\t\t}\n\n\t\tif (currentText) {\n\t\t\tchunks.push({ text: currentText, offset: currentOffset });\n\t\t}\n\t\treturn chunks.length ? chunks : [{ text, offset: 0 }];\n\t}\n\n\tprivate getHighlightMode(): \"word\" | \"sentence\" {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\treturn providerOptions.highlightMode === \"word\" ? \"word\" : \"sentence\";\n\t}\n\n\tprivate getSegmentationPolicy(): {\n\t\tuseSentenceSegmenter: boolean;\n\t\tuseWordSegmenter: boolean;\n\t\tlocale?: string;\n\t} {\n\t\tconst providerOptions = (this.config?.providerOptions || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\tconst segmenter = (providerOptions.segmenter || {}) as Record<\n\t\t\tstring,\n\t\t\tunknown\n\t\t>;\n\t\tconst mode = segmenter.mode;\n\t\tconst useSegmenter = mode !== \"regexOnly\";\n\t\tconst locale =\n\t\t\ttypeof segmenter.locale === \"string\" && segmenter.locale.trim().length > 0\n\t\t\t\t? segmenter.locale\n\t\t\t\t: typeof providerOptions.locale === \"string\" &&\n\t\t\t\t\t\tproviderOptions.locale.trim().length > 0\n\t\t\t\t\t? providerOptions.locale\n\t\t\t\t\t: undefined;\n\t\treturn {\n\t\t\tuseSentenceSegmenter: useSegmenter,\n\t\t\tuseWordSegmenter: useSegmenter,\n\t\t\tlocale,\n\t\t};\n\t}\n\n\tprivate segmentSentences(\n\t\ttext: string,\n\t): Array<{ text: string; offset: number }> {\n\t\tconst policy = this.getSegmentationPolicy();\n\t\treturn segmentTextToSentences(text, {\n\t\t\tlocale: policy.locale,\n\t\t\tuseSentenceSegmenter: policy.useSentenceSegmenter,\n\t\t});\n\t}\n\n\tprivate inferWordLength(text: string, index: number): number {\n\t\tconst safeIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(index, Math.max(0, text.length - 1)),\n\t\t);\n\t\tconst slice = text.slice(safeIndex);\n\t\tconst match = slice.match(/^\\s*([^\\s]+)/);\n\t\treturn match?.[1]?.length || 1;\n\t}\n\n\tprivate async speakChunk(\n\t\tchunkText: string,\n\t\tchunkOffset: number,\n\t\trunId: number,\n\t): Promise<boolean> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\tresolve(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst utterance = new SpeechSynthesisUtterance(chunkText);\n\t\t\tthis.utterance = utterance;\n\n\t\t\t// Apply config\n\t\t\tconst voice = findBrowserVoice(\n\t\t\t\tspeechSynthesis.getVoices(),\n\t\t\t\tthis.config?.voice,\n\t\t\t);\n\t\t\tif (voice && shouldAssignBrowserVoice(voice)) utterance.voice = voice;\n\n\t\t\tif (this.config?.rate) utterance.rate = this.config.rate;\n\t\t\tif (this.config?.pitch) utterance.pitch = this.config.pitch;\n\n\t\t\tconst clearOwnedUtterance = () => {\n\t\t\t\tif (this.utterance === utterance) {\n\t\t\t\t\tthis.utterance = null;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tutterance.onstart = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPlaying = true;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tutterance.onend = () => {\n\t\t\t\tclearOwnedUtterance();\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tresolve(true);\n\t\t\t};\n\n\t\t\tutterance.onerror = (event) => {\n\t\t\t\tclearOwnedUtterance();\n\t\t\t\tif (runId !== this.speakRunId) {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis._isPlaying = false;\n\t\t\t\tthis._isPaused = false;\n\t\t\t\tif (event.error === \"interrupted\" || event.error === \"canceled\") {\n\t\t\t\t\tresolve(false);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treject(new Error(`Speech synthesis error: ${event.error}`));\n\t\t\t};\n\n\t\t\tutterance.onpause = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = true;\n\t\t\t};\n\n\t\t\tutterance.onresume = () => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tthis._isPaused = false;\n\t\t\t};\n\n\t\t\tutterance.onboundary = (event) => {\n\t\t\t\tif (runId !== this.speakRunId) return;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Boundary event:\",\n\t\t\t\t\tevent.name,\n\t\t\t\t\t\"charIndex:\",\n\t\t\t\t\tevent.charIndex,\n\t\t\t\t\t\"charLength:\",\n\t\t\t\t\tevent.charLength,\n\t\t\t\t);\n\t\t\t\tif (event.name !== \"word\" || !this.onWordBoundary) return;\n\t\t\t\tif (this.getHighlightMode() === \"sentence\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst charIndex = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\tMath.min(event.charIndex, Math.max(0, chunkText.length - 1)),\n\t\t\t\t);\n\t\t\t\tconst reportedLength = Number(event.charLength || 0);\n\t\t\t\tconst inferredLength = this.inferWordLength(chunkText, charIndex);\n\t\t\t\tconst wordLength =\n\t\t\t\t\tNumber.isFinite(reportedLength) &&\n\t\t\t\t\treportedLength > 0 &&\n\t\t\t\t\treportedLength <= 80 &&\n\t\t\t\t\tcharIndex + reportedLength <= chunkText.length\n\t\t\t\t\t\t? reportedLength\n\t\t\t\t\t\t: inferredLength;\n\t\t\t\tconst word = chunkText\n\t\t\t\t\t.substring(\n\t\t\t\t\t\tcharIndex,\n\t\t\t\t\t\tMath.min(chunkText.length, charIndex + wordLength),\n\t\t\t\t\t)\n\t\t\t\t\t.trim();\n\t\t\t\tconst absoluteBoundaryStart = chunkOffset + charIndex;\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"[BrowserProvider] Calling onWordBoundary with word:\",\n\t\t\t\t\tword,\n\t\t\t\t\t\"at position:\",\n\t\t\t\t\tabsoluteBoundaryStart,\n\t\t\t\t);\n\t\t\t\tthis.onWordBoundary(word, absoluteBoundaryStart, wordLength);\n\t\t\t};\n\n\t\t\tspeechSynthesis.speak(utterance);\n\t\t});\n\t}\n\n\tpause(): void {\n\t\tif (this._isPlaying && !this._isPaused) {\n\t\t\tspeechSynthesis.pause();\n\t\t}\n\t}\n\n\tresume(): void {\n\t\tif (this._isPlaying && this._isPaused) {\n\t\t\tspeechSynthesis.resume();\n\t\t}\n\t}\n\n\tstop(): void {\n\t\tthis.speakRunId += 1;\n\t\tif (this._isPlaying || this.utterance) {\n\t\t\tspeechSynthesis.cancel();\n\t\t}\n\t\tthis.utterance = null;\n\t\tthis._isPlaying = false;\n\t\tthis._isPaused = false;\n\t}\n\n\tisPlaying(): boolean {\n\t\treturn this._isPlaying && !this._isPaused;\n\t}\n\n\tisPaused(): boolean {\n\t\treturn this._isPaused;\n\t}\n\n\t/**\n\t * Update settings dynamically (rate, pitch, voice)\n\t * Changes take effect on the next speak() call\n\t */\n\tupdateSettings(settings: Partial<TTSConfig>): void {\n\t\tif (!this.config) {\n\t\t\tthis.config = {} as TTSConfig;\n\t\t}\n\n\t\t// Update config with new settings\n\t\tif (settings.rate !== undefined) {\n\t\t\tthis.config.rate = settings.rate;\n\t\t}\n\t\tif (settings.pitch !== undefined) {\n\t\t\tthis.config.pitch = settings.pitch;\n\t\t}\n\t\tif (settings.voice !== undefined) {\n\t\t\tthis.config.voice = settings.voice;\n\t\t}\n\t\tif (settings.providerOptions !== undefined) {\n\t\t\tthis.config.providerOptions = {\n\t\t\t\t...(this.config.providerOptions || {}),\n\t\t\t\t...(settings.providerOptions || {}),\n\t\t\t};\n\t\t}\n\t}\n\n\tonWordBoundary?: (word: string, position: number, length?: number) => void;\n}\n"]}
@@ -91,7 +91,8 @@ const createCandidates = (spokenTokens, visibleTokens) => {
91
91
  const computeAnchors = (spokenTokens, visibleTokens) => {
92
92
  const candidates = createCandidates(spokenTokens, visibleTokens).sort((left, right) => spokenTokens[left.spokenStartToken].start -
93
93
  spokenTokens[right.spokenStartToken].start ||
94
- visibleTokens[left.visibleToken].start - visibleTokens[right.visibleToken].start);
94
+ visibleTokens[left.visibleToken].start -
95
+ visibleTokens[right.visibleToken].start);
95
96
  const bestScore = new Array(candidates.length).fill(0);
96
97
  const previous = new Array(candidates.length).fill(-1);
97
98
  for (let i = 0; i < candidates.length; i++) {
@@ -142,7 +143,8 @@ const computeAnchors = (spokenTokens, visibleTokens) => {
142
143
  const scoreConfidence = (anchors, spokenTokens, visibleTokens) => {
143
144
  if (anchors.length === 0)
144
145
  return 0;
145
- const tokenCoverage = anchors.length / Math.max(1, Math.min(spokenTokens.length, visibleTokens.length));
146
+ const tokenCoverage = anchors.length /
147
+ Math.max(1, Math.min(spokenTokens.length, visibleTokens.length));
146
148
  const averageScore = anchors.reduce((total, anchor) => total + anchor.score, 0) /
147
149
  anchors.length /
148
150
  10;
@@ -242,7 +244,10 @@ const resolvePlainBoundaryOffset = (alignment, position, safeLength) => {
242
244
  const token = findTokenAtOffset(alignment.spokenTokens, position);
243
245
  if (!token)
244
246
  return null;
245
- return { start: position, length: Math.min(safeLength, token.end - position) };
247
+ return {
248
+ start: position,
249
+ length: Math.min(safeLength, token.end - position),
250
+ };
246
251
  };
247
252
  export const resolveSpokenBoundaryOffset = (alignment, position, length = 1, boundaryWord) => {
248
253
  if (alignment.boundaryOffsetMode === "unsupported" ||
@@ -1 +1 @@
1
- {"version":3,"file":"catalog-span-alignment.js","sourceRoot":"","sources":["../../../src/services/tts/catalog-span-alignment.ts"],"names":[],"mappings":"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","sourcesContent":["import { extractSpokenText } from \"./ssml/spoken-text.js\";\nimport {\n\tcreateSpeechAlignmentTokenPattern,\n\tnormalizeTextForSpeech,\n} from \"./text-processing.js\";\n\nexport type CatalogChunkPlaybackMode =\n\t| \"exact-word\"\n\t| \"anchor-span\"\n\t| \"region-fallback\";\n\nexport type BoundaryOffsetMode =\n\t| \"plain-spoken-text\"\n\t| \"raw-ssml\"\n\t| \"unsupported\";\n\nexport interface CatalogTextToken {\n\ttext: string;\n\tnormalized: string;\n\tstart: number;\n\tend: number;\n}\n\nexport interface CatalogSpanAnchor {\n\tspokenStart: number;\n\tspokenEnd: number;\n\tvisibleStart: number;\n\tvisibleEnd: number;\n\tscore: number;\n}\n\nexport interface CatalogSpanAlignment {\n\tspeechText: string;\n\tspokenText: string;\n\tvisibleText: string;\n\tplaybackMode: CatalogChunkPlaybackMode;\n\tboundaryOffsetMode: BoundaryOffsetMode;\n\tanchors: CatalogSpanAnchor[];\n\tconfidence: number;\n\tspokenTokens: CatalogTextToken[];\n\tvisibleTokens: CatalogTextToken[];\n\trawToSpokenOffsetMap: Map<number, number>;\n}\n\ninterface MatchCandidate {\n\tspokenStartToken: number;\n\tspokenEndToken: number;\n\tvisibleToken: number;\n\tscore: number;\n}\n\n// Shared Unicode-aware tokenizer (see `createSpeechAlignmentTokenPattern`), so\n// accented Latin and non-Latin words tokenize the same way here as in the math\n// speech tokenizer. LIMITATION (i18n): the phrase table below is still English,\n// so multi-word operator phrases (\"plus or minus\", \"divided by\") only align for\n// English speech; other locales degrade to coarse region highlighting. A fresh\n// pattern per call because of the global flag's mutable `lastIndex`.\nconst TOKEN_PATTERN = createSpeechAlignmentTokenPattern();\n\nconst NUMERIC_WORDS = new Map<string, string>([\n\t[\"zero\", \"0\"],\n\t[\"one\", \"1\"],\n\t[\"two\", \"2\"],\n\t[\"three\", \"3\"],\n\t[\"four\", \"4\"],\n\t[\"five\", \"5\"],\n\t[\"six\", \"6\"],\n\t[\"seven\", \"7\"],\n\t[\"eight\", \"8\"],\n\t[\"nine\", \"9\"],\n\t[\"ten\", \"10\"],\n]);\n\nconst SPOKEN_PHRASE_TARGETS: Array<{\n\tphrase: string[];\n\tvisible: string;\n\tscore: number;\n}> = [\n\t{ phrase: [\"plus\", \"or\", \"minus\"], visible: \"±\", score: 8 },\n\t{ phrase: [\"square\", \"root\"], visible: \"√\", score: 8 },\n\t{ phrase: [\"divided\", \"by\"], visible: \"/\", score: 7 },\n\t{ phrase: [\"all\", \"over\"], visible: \"/\", score: 7 },\n\t{ phrase: [\"equals\"], visible: \"=\", score: 7 },\n\t{ phrase: [\"equal\"], visible: \"=\", score: 7 },\n\t{ phrase: [\"times\"], visible: \"\\u2062\", score: 7 },\n\t{ phrase: [\"multiplied\", \"by\"], visible: \"\\u2062\", score: 7 },\n\t{ phrase: [\"over\"], visible: \"/\", score: 6 },\n\t{ phrase: [\"squared\"], visible: \"²\", score: 6 },\n\t{ phrase: [\"squared\"], visible: \"2\", score: 4 },\n\t{ phrase: [\"negative\"], visible: \"-\", score: 5 },\n\t{ phrase: [\"minus\"], visible: \"-\", score: 5 },\n\t{ phrase: [\"plus\"], visible: \"+\", score: 5 },\n];\n\nconst tokenize = (text: string): CatalogTextToken[] => {\n\tconst tokens: CatalogTextToken[] = [];\n\tfor (const match of text.matchAll(TOKEN_PATTERN)) {\n\t\tconst value = match[0];\n\t\tconst start = match.index ?? 0;\n\t\ttokens.push({\n\t\t\ttext: value,\n\t\t\tnormalized: value.toLowerCase(),\n\t\t\tstart,\n\t\t\tend: start + value.length,\n\t\t});\n\t}\n\treturn tokens;\n};\n\nconst normalizedTokenValue = (token: CatalogTextToken): string =>\n\tNUMERIC_WORDS.get(token.normalized) || token.normalized;\n\nconst createCandidates = (\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): MatchCandidate[] => {\n\tconst candidates: MatchCandidate[] = [];\n\tfor (let spokenIndex = 0; spokenIndex < spokenTokens.length; spokenIndex++) {\n\t\tfor (let visibleIndex = 0; visibleIndex < visibleTokens.length; visibleIndex++) {\n\t\t\tconst spokenValue = normalizedTokenValue(spokenTokens[spokenIndex]);\n\t\t\tconst visibleValue = normalizedTokenValue(visibleTokens[visibleIndex]);\n\t\t\tif (spokenValue === visibleValue) {\n\t\t\t\tcandidates.push({\n\t\t\t\t\tspokenStartToken: spokenIndex,\n\t\t\t\t\tspokenEndToken: spokenIndex + 1,\n\t\t\t\t\tvisibleToken: visibleIndex,\n\t\t\t\t\tscore: 10,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tfor (const phraseTarget of SPOKEN_PHRASE_TARGETS) {\n\t\t\tconst phraseTokens = spokenTokens.slice(\n\t\t\t\tspokenIndex,\n\t\t\t\tspokenIndex + phraseTarget.phrase.length,\n\t\t\t);\n\t\t\tif (phraseTokens.length !== phraseTarget.phrase.length) continue;\n\t\t\tif (\n\t\t\t\t!phraseTokens.every(\n\t\t\t\t\t(token, tokenIndex) =>\n\t\t\t\t\t\ttoken.normalized === phraseTarget.phrase[tokenIndex],\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (\n\t\t\t\tlet visibleIndex = 0;\n\t\t\t\tvisibleIndex < visibleTokens.length;\n\t\t\t\tvisibleIndex++\n\t\t\t) {\n\t\t\t\tif (visibleTokens[visibleIndex].normalized !== phraseTarget.visible) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcandidates.push({\n\t\t\t\t\tspokenStartToken: spokenIndex,\n\t\t\t\t\tspokenEndToken: spokenIndex + phraseTarget.phrase.length,\n\t\t\t\t\tvisibleToken: visibleIndex,\n\t\t\t\t\tscore: phraseTarget.score,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn candidates;\n};\n\nconst computeAnchors = (\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): CatalogSpanAnchor[] => {\n\tconst candidates = createCandidates(spokenTokens, visibleTokens).sort(\n\t\t(left, right) =>\n\t\t\tspokenTokens[left.spokenStartToken].start -\n\t\t\t\tspokenTokens[right.spokenStartToken].start ||\n\t\t\tvisibleTokens[left.visibleToken].start - visibleTokens[right.visibleToken].start,\n\t);\n\tconst bestScore = new Array(candidates.length).fill(0) as number[];\n\tconst previous = new Array(candidates.length).fill(-1) as number[];\n\n\tfor (let i = 0; i < candidates.length; i++) {\n\t\tconst candidate = candidates[i];\n\t\tbestScore[i] = candidate.score;\n\t\tfor (let j = 0; j < i; j++) {\n\t\t\tconst prior = candidates[j];\n\t\t\tif (\n\t\t\t\tprior.spokenEndToken <= candidate.spokenStartToken &&\n\t\t\t\tprior.visibleToken < candidate.visibleToken\n\t\t\t) {\n\t\t\t\tconst spokenGap = candidate.spokenStartToken - prior.spokenEndToken;\n\t\t\t\tconst visibleGap = candidate.visibleToken - prior.visibleToken - 1;\n\t\t\t\tconst gapPenalty = Math.max(0, Math.abs(spokenGap - visibleGap) - 2);\n\t\t\t\tconst score = bestScore[j] + candidate.score - gapPenalty;\n\t\t\t\tif (score > bestScore[i]) {\n\t\t\t\t\tbestScore[i] = score;\n\t\t\t\t\tprevious[i] = j;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlet bestIndex = -1;\n\tfor (let i = 0; i < bestScore.length; i++) {\n\t\tif (bestIndex < 0 || bestScore[i] > bestScore[bestIndex]) {\n\t\t\tbestIndex = i;\n\t\t}\n\t}\n\tif (bestIndex < 0) return [];\n\n\tconst chosen: MatchCandidate[] = [];\n\tlet current = bestIndex;\n\twhile (current >= 0) {\n\t\tchosen.unshift(candidates[current]);\n\t\tcurrent = previous[current];\n\t}\n\n\treturn chosen.map((candidate) => {\n\t\tconst firstSpoken = spokenTokens[candidate.spokenStartToken];\n\t\tconst lastSpoken = spokenTokens[candidate.spokenEndToken - 1];\n\t\tconst visible = visibleTokens[candidate.visibleToken];\n\t\treturn {\n\t\t\tspokenStart: firstSpoken.start,\n\t\t\tspokenEnd: lastSpoken.end,\n\t\t\tvisibleStart: visible.start,\n\t\t\tvisibleEnd: visible.end,\n\t\t\tscore: candidate.score,\n\t\t};\n\t});\n};\n\nconst scoreConfidence = (\n\tanchors: CatalogSpanAnchor[],\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): number => {\n\tif (anchors.length === 0) return 0;\n\tconst tokenCoverage =\n\t\tanchors.length / Math.max(1, Math.min(spokenTokens.length, visibleTokens.length));\n\tconst averageScore =\n\t\tanchors.reduce((total, anchor) => total + anchor.score, 0) /\n\t\tanchors.length /\n\t\t10;\n\treturn Math.min(1, tokenCoverage * averageScore);\n};\n\nexport const createCatalogSpanAlignment = (args: {\n\tspeechText: string;\n\tvisibleText: string;\n}): CatalogSpanAlignment => {\n\tconst extracted = extractSpokenText(args.speechText);\n\tconst visibleText = normalizeTextForSpeech(args.visibleText);\n\tconst spokenText = extracted.spokenText;\n\tconst spokenTokens = tokenize(spokenText);\n\tconst visibleTokens = tokenize(visibleText);\n\tconst anchors = extracted.unsupportedSemantic\n\t\t? []\n\t\t: computeAnchors(spokenTokens, visibleTokens);\n\tconst confidence = scoreConfidence(anchors, spokenTokens, visibleTokens);\n\tconst isExact = spokenText === visibleText && spokenText.length > 0;\n\tconst hasShortSingleExactAnchor =\n\t\tanchors.length === 1 &&\n\t\tanchors[0].score === 10 &&\n\t\tspokenTokens.length <= 2 &&\n\t\tvisibleTokens.length <= 2;\n\tconst playbackMode: CatalogChunkPlaybackMode = extracted.unsupportedSemantic\n\t\t? \"region-fallback\"\n\t\t: isExact\n\t\t\t? \"exact-word\"\n\t\t\t: anchors.length >= 2 || hasShortSingleExactAnchor\n\t\t\t\t? confidence >= 0.12\n\t\t\t\t\t? \"anchor-span\"\n\t\t\t\t\t: \"region-fallback\"\n\t\t\t\t: \"region-fallback\";\n\tconst boundaryOffsetMode: BoundaryOffsetMode = extracted.unsupportedSemantic\n\t\t? \"unsupported\"\n\t\t: extracted.hasMarkup\n\t\t\t? \"raw-ssml\"\n\t\t\t: \"plain-spoken-text\";\n\n\treturn {\n\t\tspeechText: args.speechText,\n\t\tspokenText,\n\t\tvisibleText,\n\t\tplaybackMode,\n\t\tboundaryOffsetMode,\n\t\tanchors,\n\t\tconfidence,\n\t\tspokenTokens,\n\t\tvisibleTokens,\n\t\trawToSpokenOffsetMap: extracted.rawToSpokenOffsetMap,\n\t};\n};\n\nconst findTokenAtOffset = (\n\ttokens: CatalogTextToken[],\n\toffset: number,\n): CatalogTextToken | null =>\n\ttokens.find((token) => token.start <= offset && offset < token.end) || null;\n\nconst normalizeBoundaryWord = (word?: string): string | null => {\n\tif (!word) return null;\n\tif (/^<[^>]+>$/.test(word.trim())) return null;\n\tconst token = tokenize(word)[0];\n\tif (!token) return null;\n\treturn NUMERIC_WORDS.get(token.normalized) || token.normalized;\n};\n\nconst candidateMatchesBoundaryWord = (\n\talignment: CatalogSpanAlignment,\n\tcandidate: { start: number; length: number } | null,\n\tboundaryWord?: string,\n): boolean => {\n\tconst normalizedBoundary = normalizeBoundaryWord(boundaryWord);\n\tif (!candidate || !normalizedBoundary) return false;\n\tconst token = findTokenAtOffset(alignment.spokenTokens, candidate.start);\n\tif (!token) return false;\n\treturn normalizedTokenValue(token) === normalizedBoundary;\n};\n\nconst mapRawOffsetToSpokenOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n): number | null => {\n\tconst direct = alignment.rawToSpokenOffsetMap.get(position);\n\tif (direct !== undefined) return direct;\n\tfor (let offset = position; offset < alignment.speechText.length; offset++) {\n\t\tconst mapped = alignment.rawToSpokenOffsetMap.get(offset);\n\t\tif (mapped !== undefined) return mapped;\n\t\tif (alignment.speechText[offset] === \">\") break;\n\t}\n\treturn null;\n};\n\nconst resolveRawBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tsafeLength: number,\n): { start: number; length: number } | null => {\n\tconst mappedStart = mapRawOffsetToSpokenOffset(alignment, position);\n\tif (mappedStart === null) return null;\n\tconst mappedEnd = mapRawOffsetToSpokenOffset(\n\t\talignment,\n\t\tMath.max(position, position + safeLength - 1),\n\t);\n\tconst token = findTokenAtOffset(alignment.spokenTokens, mappedStart);\n\tconst mappedLength =\n\t\tmappedEnd !== null && mappedEnd >= mappedStart\n\t\t\t? mappedEnd - mappedStart + 1\n\t\t\t: token\n\t\t\t\t? token.end - mappedStart\n\t\t\t\t: safeLength;\n\treturn { start: mappedStart, length: Math.max(1, mappedLength) };\n};\n\nconst resolvePlainBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tsafeLength: number,\n): { start: number; length: number } | null => {\n\tif (position < 0 || position >= alignment.spokenText.length) return null;\n\tconst token = findTokenAtOffset(alignment.spokenTokens, position);\n\tif (!token) return null;\n\treturn { start: position, length: Math.min(safeLength, token.end - position) };\n};\n\nexport const resolveSpokenBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tlength = 1,\n\tboundaryWord?: string,\n): { start: number; length: number } | null => {\n\tif (\n\t\talignment.boundaryOffsetMode === \"unsupported\" ||\n\t\t!Number.isFinite(position)\n\t) {\n\t\treturn null;\n\t}\n\tconst safeLength = Math.max(1, Number.isFinite(length) ? length : 1);\n\tif (boundaryWord && /^<[^>]+>$/.test(boundaryWord.trim())) return null;\n\tif (alignment.boundaryOffsetMode === \"raw-ssml\") {\n\t\tconst rawCandidate = resolveRawBoundaryOffset(\n\t\t\talignment,\n\t\t\tposition,\n\t\t\tsafeLength,\n\t\t);\n\t\tconst plainCandidate = resolvePlainBoundaryOffset(\n\t\t\talignment,\n\t\t\tposition,\n\t\t\tsafeLength,\n\t\t);\n\t\tif (boundaryWord) {\n\t\t\tif (\n\t\t\t\tcandidateMatchesBoundaryWord(alignment, rawCandidate, boundaryWord)\n\t\t\t) {\n\t\t\t\treturn rawCandidate;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcandidateMatchesBoundaryWord(alignment, plainCandidate, boundaryWord)\n\t\t\t) {\n\t\t\t\treturn plainCandidate;\n\t\t\t}\n\t\t\tif (normalizeBoundaryWord(boundaryWord)) return null;\n\t\t}\n\t\treturn rawCandidate || plainCandidate;\n\t}\n\treturn (\n\t\tresolvePlainBoundaryOffset(alignment, position, safeLength) ||\n\t\tresolveRawBoundaryOffset(alignment, position, safeLength)\n\t);\n};\n\nexport const resolveVisibleSpanForBoundary = (\n\talignment: CatalogSpanAlignment,\n\tspokenOffset: number,\n): { start: number; end: number } | null => {\n\tif (alignment.playbackMode === \"region-fallback\") return null;\n\tconst exactAnchor = alignment.anchors.find(\n\t\t(anchor) => anchor.spokenStart <= spokenOffset && spokenOffset < anchor.spokenEnd,\n\t);\n\tif (exactAnchor) {\n\t\treturn { start: exactAnchor.visibleStart, end: exactAnchor.visibleEnd };\n\t}\n\n\tlet previous: CatalogSpanAnchor | null = null;\n\tlet next: CatalogSpanAnchor | null = null;\n\tfor (const anchor of alignment.anchors) {\n\t\tif (anchor.spokenEnd <= spokenOffset) {\n\t\t\tprevious = anchor;\n\t\t\tcontinue;\n\t\t}\n\t\tif (anchor.spokenStart > spokenOffset) {\n\t\t\tnext = anchor;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (previous && next) {\n\t\treturn {\n\t\t\tstart: Math.min(previous.visibleStart, next.visibleStart),\n\t\t\tend: Math.max(previous.visibleEnd, next.visibleEnd),\n\t\t};\n\t}\n\treturn null;\n};\n"]}
1
+ {"version":3,"file":"catalog-span-alignment.js","sourceRoot":"","sources":["../../../src/services/tts/catalog-span-alignment.ts"],"names":[],"mappings":"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","sourcesContent":["import { extractSpokenText } from \"./ssml/spoken-text.js\";\nimport {\n\tcreateSpeechAlignmentTokenPattern,\n\tnormalizeTextForSpeech,\n} from \"./text-processing.js\";\n\nexport type CatalogChunkPlaybackMode =\n\t| \"exact-word\"\n\t| \"anchor-span\"\n\t| \"region-fallback\";\n\nexport type BoundaryOffsetMode =\n\t| \"plain-spoken-text\"\n\t| \"raw-ssml\"\n\t| \"unsupported\";\n\nexport interface CatalogTextToken {\n\ttext: string;\n\tnormalized: string;\n\tstart: number;\n\tend: number;\n}\n\nexport interface CatalogSpanAnchor {\n\tspokenStart: number;\n\tspokenEnd: number;\n\tvisibleStart: number;\n\tvisibleEnd: number;\n\tscore: number;\n}\n\nexport interface CatalogSpanAlignment {\n\tspeechText: string;\n\tspokenText: string;\n\tvisibleText: string;\n\tplaybackMode: CatalogChunkPlaybackMode;\n\tboundaryOffsetMode: BoundaryOffsetMode;\n\tanchors: CatalogSpanAnchor[];\n\tconfidence: number;\n\tspokenTokens: CatalogTextToken[];\n\tvisibleTokens: CatalogTextToken[];\n\trawToSpokenOffsetMap: Map<number, number>;\n}\n\ninterface MatchCandidate {\n\tspokenStartToken: number;\n\tspokenEndToken: number;\n\tvisibleToken: number;\n\tscore: number;\n}\n\n// Shared Unicode-aware tokenizer (see `createSpeechAlignmentTokenPattern`), so\n// accented Latin and non-Latin words tokenize the same way here as in the math\n// speech tokenizer. LIMITATION (i18n): the phrase table below is still English,\n// so multi-word operator phrases (\"plus or minus\", \"divided by\") only align for\n// English speech; other locales degrade to coarse region highlighting. A fresh\n// pattern per call because of the global flag's mutable `lastIndex`.\nconst TOKEN_PATTERN = createSpeechAlignmentTokenPattern();\n\nconst NUMERIC_WORDS = new Map<string, string>([\n\t[\"zero\", \"0\"],\n\t[\"one\", \"1\"],\n\t[\"two\", \"2\"],\n\t[\"three\", \"3\"],\n\t[\"four\", \"4\"],\n\t[\"five\", \"5\"],\n\t[\"six\", \"6\"],\n\t[\"seven\", \"7\"],\n\t[\"eight\", \"8\"],\n\t[\"nine\", \"9\"],\n\t[\"ten\", \"10\"],\n]);\n\nconst SPOKEN_PHRASE_TARGETS: Array<{\n\tphrase: string[];\n\tvisible: string;\n\tscore: number;\n}> = [\n\t{ phrase: [\"plus\", \"or\", \"minus\"], visible: \"±\", score: 8 },\n\t{ phrase: [\"square\", \"root\"], visible: \"√\", score: 8 },\n\t{ phrase: [\"divided\", \"by\"], visible: \"/\", score: 7 },\n\t{ phrase: [\"all\", \"over\"], visible: \"/\", score: 7 },\n\t{ phrase: [\"equals\"], visible: \"=\", score: 7 },\n\t{ phrase: [\"equal\"], visible: \"=\", score: 7 },\n\t{ phrase: [\"times\"], visible: \"\\u2062\", score: 7 },\n\t{ phrase: [\"multiplied\", \"by\"], visible: \"\\u2062\", score: 7 },\n\t{ phrase: [\"over\"], visible: \"/\", score: 6 },\n\t{ phrase: [\"squared\"], visible: \"²\", score: 6 },\n\t{ phrase: [\"squared\"], visible: \"2\", score: 4 },\n\t{ phrase: [\"negative\"], visible: \"-\", score: 5 },\n\t{ phrase: [\"minus\"], visible: \"-\", score: 5 },\n\t{ phrase: [\"plus\"], visible: \"+\", score: 5 },\n];\n\nconst tokenize = (text: string): CatalogTextToken[] => {\n\tconst tokens: CatalogTextToken[] = [];\n\tfor (const match of text.matchAll(TOKEN_PATTERN)) {\n\t\tconst value = match[0];\n\t\tconst start = match.index ?? 0;\n\t\ttokens.push({\n\t\t\ttext: value,\n\t\t\tnormalized: value.toLowerCase(),\n\t\t\tstart,\n\t\t\tend: start + value.length,\n\t\t});\n\t}\n\treturn tokens;\n};\n\nconst normalizedTokenValue = (token: CatalogTextToken): string =>\n\tNUMERIC_WORDS.get(token.normalized) || token.normalized;\n\nconst createCandidates = (\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): MatchCandidate[] => {\n\tconst candidates: MatchCandidate[] = [];\n\tfor (let spokenIndex = 0; spokenIndex < spokenTokens.length; spokenIndex++) {\n\t\tfor (\n\t\t\tlet visibleIndex = 0;\n\t\t\tvisibleIndex < visibleTokens.length;\n\t\t\tvisibleIndex++\n\t\t) {\n\t\t\tconst spokenValue = normalizedTokenValue(spokenTokens[spokenIndex]);\n\t\t\tconst visibleValue = normalizedTokenValue(visibleTokens[visibleIndex]);\n\t\t\tif (spokenValue === visibleValue) {\n\t\t\t\tcandidates.push({\n\t\t\t\t\tspokenStartToken: spokenIndex,\n\t\t\t\t\tspokenEndToken: spokenIndex + 1,\n\t\t\t\t\tvisibleToken: visibleIndex,\n\t\t\t\t\tscore: 10,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tfor (const phraseTarget of SPOKEN_PHRASE_TARGETS) {\n\t\t\tconst phraseTokens = spokenTokens.slice(\n\t\t\t\tspokenIndex,\n\t\t\t\tspokenIndex + phraseTarget.phrase.length,\n\t\t\t);\n\t\t\tif (phraseTokens.length !== phraseTarget.phrase.length) continue;\n\t\t\tif (\n\t\t\t\t!phraseTokens.every(\n\t\t\t\t\t(token, tokenIndex) =>\n\t\t\t\t\t\ttoken.normalized === phraseTarget.phrase[tokenIndex],\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (\n\t\t\t\tlet visibleIndex = 0;\n\t\t\t\tvisibleIndex < visibleTokens.length;\n\t\t\t\tvisibleIndex++\n\t\t\t) {\n\t\t\t\tif (visibleTokens[visibleIndex].normalized !== phraseTarget.visible) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tcandidates.push({\n\t\t\t\t\tspokenStartToken: spokenIndex,\n\t\t\t\t\tspokenEndToken: spokenIndex + phraseTarget.phrase.length,\n\t\t\t\t\tvisibleToken: visibleIndex,\n\t\t\t\t\tscore: phraseTarget.score,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn candidates;\n};\n\nconst computeAnchors = (\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): CatalogSpanAnchor[] => {\n\tconst candidates = createCandidates(spokenTokens, visibleTokens).sort(\n\t\t(left, right) =>\n\t\t\tspokenTokens[left.spokenStartToken].start -\n\t\t\t\tspokenTokens[right.spokenStartToken].start ||\n\t\t\tvisibleTokens[left.visibleToken].start -\n\t\t\t\tvisibleTokens[right.visibleToken].start,\n\t);\n\tconst bestScore = new Array(candidates.length).fill(0) as number[];\n\tconst previous = new Array(candidates.length).fill(-1) as number[];\n\n\tfor (let i = 0; i < candidates.length; i++) {\n\t\tconst candidate = candidates[i];\n\t\tbestScore[i] = candidate.score;\n\t\tfor (let j = 0; j < i; j++) {\n\t\t\tconst prior = candidates[j];\n\t\t\tif (\n\t\t\t\tprior.spokenEndToken <= candidate.spokenStartToken &&\n\t\t\t\tprior.visibleToken < candidate.visibleToken\n\t\t\t) {\n\t\t\t\tconst spokenGap = candidate.spokenStartToken - prior.spokenEndToken;\n\t\t\t\tconst visibleGap = candidate.visibleToken - prior.visibleToken - 1;\n\t\t\t\tconst gapPenalty = Math.max(0, Math.abs(spokenGap - visibleGap) - 2);\n\t\t\t\tconst score = bestScore[j] + candidate.score - gapPenalty;\n\t\t\t\tif (score > bestScore[i]) {\n\t\t\t\t\tbestScore[i] = score;\n\t\t\t\t\tprevious[i] = j;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlet bestIndex = -1;\n\tfor (let i = 0; i < bestScore.length; i++) {\n\t\tif (bestIndex < 0 || bestScore[i] > bestScore[bestIndex]) {\n\t\t\tbestIndex = i;\n\t\t}\n\t}\n\tif (bestIndex < 0) return [];\n\n\tconst chosen: MatchCandidate[] = [];\n\tlet current = bestIndex;\n\twhile (current >= 0) {\n\t\tchosen.unshift(candidates[current]);\n\t\tcurrent = previous[current];\n\t}\n\n\treturn chosen.map((candidate) => {\n\t\tconst firstSpoken = spokenTokens[candidate.spokenStartToken];\n\t\tconst lastSpoken = spokenTokens[candidate.spokenEndToken - 1];\n\t\tconst visible = visibleTokens[candidate.visibleToken];\n\t\treturn {\n\t\t\tspokenStart: firstSpoken.start,\n\t\t\tspokenEnd: lastSpoken.end,\n\t\t\tvisibleStart: visible.start,\n\t\t\tvisibleEnd: visible.end,\n\t\t\tscore: candidate.score,\n\t\t};\n\t});\n};\n\nconst scoreConfidence = (\n\tanchors: CatalogSpanAnchor[],\n\tspokenTokens: CatalogTextToken[],\n\tvisibleTokens: CatalogTextToken[],\n): number => {\n\tif (anchors.length === 0) return 0;\n\tconst tokenCoverage =\n\t\tanchors.length /\n\t\tMath.max(1, Math.min(spokenTokens.length, visibleTokens.length));\n\tconst averageScore =\n\t\tanchors.reduce((total, anchor) => total + anchor.score, 0) /\n\t\tanchors.length /\n\t\t10;\n\treturn Math.min(1, tokenCoverage * averageScore);\n};\n\nexport const createCatalogSpanAlignment = (args: {\n\tspeechText: string;\n\tvisibleText: string;\n}): CatalogSpanAlignment => {\n\tconst extracted = extractSpokenText(args.speechText);\n\tconst visibleText = normalizeTextForSpeech(args.visibleText);\n\tconst spokenText = extracted.spokenText;\n\tconst spokenTokens = tokenize(spokenText);\n\tconst visibleTokens = tokenize(visibleText);\n\tconst anchors = extracted.unsupportedSemantic\n\t\t? []\n\t\t: computeAnchors(spokenTokens, visibleTokens);\n\tconst confidence = scoreConfidence(anchors, spokenTokens, visibleTokens);\n\tconst isExact = spokenText === visibleText && spokenText.length > 0;\n\tconst hasShortSingleExactAnchor =\n\t\tanchors.length === 1 &&\n\t\tanchors[0].score === 10 &&\n\t\tspokenTokens.length <= 2 &&\n\t\tvisibleTokens.length <= 2;\n\tconst playbackMode: CatalogChunkPlaybackMode = extracted.unsupportedSemantic\n\t\t? \"region-fallback\"\n\t\t: isExact\n\t\t\t? \"exact-word\"\n\t\t\t: anchors.length >= 2 || hasShortSingleExactAnchor\n\t\t\t\t? confidence >= 0.12\n\t\t\t\t\t? \"anchor-span\"\n\t\t\t\t\t: \"region-fallback\"\n\t\t\t\t: \"region-fallback\";\n\tconst boundaryOffsetMode: BoundaryOffsetMode = extracted.unsupportedSemantic\n\t\t? \"unsupported\"\n\t\t: extracted.hasMarkup\n\t\t\t? \"raw-ssml\"\n\t\t\t: \"plain-spoken-text\";\n\n\treturn {\n\t\tspeechText: args.speechText,\n\t\tspokenText,\n\t\tvisibleText,\n\t\tplaybackMode,\n\t\tboundaryOffsetMode,\n\t\tanchors,\n\t\tconfidence,\n\t\tspokenTokens,\n\t\tvisibleTokens,\n\t\trawToSpokenOffsetMap: extracted.rawToSpokenOffsetMap,\n\t};\n};\n\nconst findTokenAtOffset = (\n\ttokens: CatalogTextToken[],\n\toffset: number,\n): CatalogTextToken | null =>\n\ttokens.find((token) => token.start <= offset && offset < token.end) || null;\n\nconst normalizeBoundaryWord = (word?: string): string | null => {\n\tif (!word) return null;\n\tif (/^<[^>]+>$/.test(word.trim())) return null;\n\tconst token = tokenize(word)[0];\n\tif (!token) return null;\n\treturn NUMERIC_WORDS.get(token.normalized) || token.normalized;\n};\n\nconst candidateMatchesBoundaryWord = (\n\talignment: CatalogSpanAlignment,\n\tcandidate: { start: number; length: number } | null,\n\tboundaryWord?: string,\n): boolean => {\n\tconst normalizedBoundary = normalizeBoundaryWord(boundaryWord);\n\tif (!candidate || !normalizedBoundary) return false;\n\tconst token = findTokenAtOffset(alignment.spokenTokens, candidate.start);\n\tif (!token) return false;\n\treturn normalizedTokenValue(token) === normalizedBoundary;\n};\n\nconst mapRawOffsetToSpokenOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n): number | null => {\n\tconst direct = alignment.rawToSpokenOffsetMap.get(position);\n\tif (direct !== undefined) return direct;\n\tfor (let offset = position; offset < alignment.speechText.length; offset++) {\n\t\tconst mapped = alignment.rawToSpokenOffsetMap.get(offset);\n\t\tif (mapped !== undefined) return mapped;\n\t\tif (alignment.speechText[offset] === \">\") break;\n\t}\n\treturn null;\n};\n\nconst resolveRawBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tsafeLength: number,\n): { start: number; length: number } | null => {\n\tconst mappedStart = mapRawOffsetToSpokenOffset(alignment, position);\n\tif (mappedStart === null) return null;\n\tconst mappedEnd = mapRawOffsetToSpokenOffset(\n\t\talignment,\n\t\tMath.max(position, position + safeLength - 1),\n\t);\n\tconst token = findTokenAtOffset(alignment.spokenTokens, mappedStart);\n\tconst mappedLength =\n\t\tmappedEnd !== null && mappedEnd >= mappedStart\n\t\t\t? mappedEnd - mappedStart + 1\n\t\t\t: token\n\t\t\t\t? token.end - mappedStart\n\t\t\t\t: safeLength;\n\treturn { start: mappedStart, length: Math.max(1, mappedLength) };\n};\n\nconst resolvePlainBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tsafeLength: number,\n): { start: number; length: number } | null => {\n\tif (position < 0 || position >= alignment.spokenText.length) return null;\n\tconst token = findTokenAtOffset(alignment.spokenTokens, position);\n\tif (!token) return null;\n\treturn {\n\t\tstart: position,\n\t\tlength: Math.min(safeLength, token.end - position),\n\t};\n};\n\nexport const resolveSpokenBoundaryOffset = (\n\talignment: CatalogSpanAlignment,\n\tposition: number,\n\tlength = 1,\n\tboundaryWord?: string,\n): { start: number; length: number } | null => {\n\tif (\n\t\talignment.boundaryOffsetMode === \"unsupported\" ||\n\t\t!Number.isFinite(position)\n\t) {\n\t\treturn null;\n\t}\n\tconst safeLength = Math.max(1, Number.isFinite(length) ? length : 1);\n\tif (boundaryWord && /^<[^>]+>$/.test(boundaryWord.trim())) return null;\n\tif (alignment.boundaryOffsetMode === \"raw-ssml\") {\n\t\tconst rawCandidate = resolveRawBoundaryOffset(\n\t\t\talignment,\n\t\t\tposition,\n\t\t\tsafeLength,\n\t\t);\n\t\tconst plainCandidate = resolvePlainBoundaryOffset(\n\t\t\talignment,\n\t\t\tposition,\n\t\t\tsafeLength,\n\t\t);\n\t\tif (boundaryWord) {\n\t\t\tif (candidateMatchesBoundaryWord(alignment, rawCandidate, boundaryWord)) {\n\t\t\t\treturn rawCandidate;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcandidateMatchesBoundaryWord(alignment, plainCandidate, boundaryWord)\n\t\t\t) {\n\t\t\t\treturn plainCandidate;\n\t\t\t}\n\t\t\tif (normalizeBoundaryWord(boundaryWord)) return null;\n\t\t}\n\t\treturn rawCandidate || plainCandidate;\n\t}\n\treturn (\n\t\tresolvePlainBoundaryOffset(alignment, position, safeLength) ||\n\t\tresolveRawBoundaryOffset(alignment, position, safeLength)\n\t);\n};\n\nexport const resolveVisibleSpanForBoundary = (\n\talignment: CatalogSpanAlignment,\n\tspokenOffset: number,\n): { start: number; end: number } | null => {\n\tif (alignment.playbackMode === \"region-fallback\") return null;\n\tconst exactAnchor = alignment.anchors.find(\n\t\t(anchor) =>\n\t\t\tanchor.spokenStart <= spokenOffset && spokenOffset < anchor.spokenEnd,\n\t);\n\tif (exactAnchor) {\n\t\treturn { start: exactAnchor.visibleStart, end: exactAnchor.visibleEnd };\n\t}\n\n\tlet previous: CatalogSpanAnchor | null = null;\n\tlet next: CatalogSpanAnchor | null = null;\n\tfor (const anchor of alignment.anchors) {\n\t\tif (anchor.spokenEnd <= spokenOffset) {\n\t\t\tprevious = anchor;\n\t\t\tcontinue;\n\t\t}\n\t\tif (anchor.spokenStart > spokenOffset) {\n\t\t\tnext = anchor;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (previous && next) {\n\t\treturn {\n\t\t\tstart: Math.min(previous.visibleStart, next.visibleStart),\n\t\t\tend: Math.max(previous.visibleEnd, next.visibleEnd),\n\t\t};\n\t}\n\treturn null;\n};\n"]}