@runtypelabs/persona 3.34.1 → 3.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +11 -11
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
  5. package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +11 -11
  9. package/dist/codegen.js +10 -10
  10. package/dist/index.cjs +46 -44
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +514 -158
  13. package/dist/index.d.ts +514 -158
  14. package/dist/index.global.js +41 -89
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +46 -44
  17. package/dist/index.js.map +1 -1
  18. package/dist/install.global.js +1 -1
  19. package/dist/install.global.js.map +1 -1
  20. package/dist/launcher.global.js +2 -2
  21. package/dist/launcher.global.js.map +1 -1
  22. package/dist/markdown-parsers.js +53 -0
  23. package/dist/plugin-kit.d.cts +4 -4
  24. package/dist/plugin-kit.d.ts +4 -4
  25. package/dist/runtype-tts.js +1 -0
  26. package/dist/smart-dom-reader.cjs +1 -1
  27. package/dist/smart-dom-reader.d.cts +352 -103
  28. package/dist/smart-dom-reader.d.ts +352 -103
  29. package/dist/smart-dom-reader.js +1 -1
  30. package/dist/theme-editor-preview.cjs +203 -0
  31. package/dist/theme-editor-preview.d.cts +5748 -0
  32. package/dist/theme-editor-preview.d.ts +5748 -0
  33. package/dist/theme-editor-preview.js +203 -0
  34. package/dist/theme-editor.cjs +14 -131
  35. package/dist/theme-editor.d.cts +362 -574
  36. package/dist/theme-editor.d.ts +362 -574
  37. package/dist/theme-editor.js +14 -131
  38. package/dist/theme-reference.cjs +1 -1
  39. package/dist/theme-reference.d.cts +1 -1
  40. package/dist/theme-reference.d.ts +1 -1
  41. package/dist/theme-reference.js +1 -1
  42. package/dist/voice-worklet-player.cjs +80 -0
  43. package/dist/voice-worklet-player.d.cts +215 -0
  44. package/dist/voice-worklet-player.d.ts +215 -0
  45. package/dist/voice-worklet-player.js +80 -0
  46. package/dist/widget.css +54 -36
  47. package/package.json +16 -2
  48. package/src/animations/glyph-cycle.ts +11 -14
  49. package/src/animations/wipe.ts +1 -1
  50. package/src/ask-user-question-tool.test.ts +1 -1
  51. package/src/ask-user-question-tool.ts +6 -8
  52. package/src/client.test.ts +12 -12
  53. package/src/client.ts +30 -31
  54. package/src/codegen.test.ts +1 -1
  55. package/src/codegen.ts +1 -1
  56. package/src/components/approval-bubble.test.ts +1 -1
  57. package/src/components/approval-bubble.ts +3 -3
  58. package/src/components/artifact-pane.ts +3 -3
  59. package/src/components/ask-user-question-bubble.test.ts +14 -14
  60. package/src/components/ask-user-question-bubble.ts +10 -10
  61. package/src/components/composer-builder.ts +17 -16
  62. package/src/components/composer-parts.test.ts +54 -0
  63. package/src/components/composer-parts.ts +95 -126
  64. package/src/components/demo-carousel.ts +1 -1
  65. package/src/components/event-stream-view.test.ts +7 -7
  66. package/src/components/event-stream-view.ts +5 -5
  67. package/src/components/header-builder.ts +33 -27
  68. package/src/components/header-layouts.ts +1 -1
  69. package/src/components/header-parts.test.ts +90 -0
  70. package/src/components/header-parts.ts +62 -93
  71. package/src/components/launcher.test.ts +11 -0
  72. package/src/components/launcher.ts +1 -1
  73. package/src/components/message-bubble.test.ts +74 -2
  74. package/src/components/message-bubble.ts +25 -11
  75. package/src/components/messages.ts +2 -2
  76. package/src/components/panel.test.ts +1 -1
  77. package/src/components/panel.ts +92 -97
  78. package/src/components/pill-composer-builder.ts +28 -24
  79. package/src/components/reasoning-bubble.ts +2 -2
  80. package/src/components/suggestions.ts +1 -1
  81. package/src/components/tool-bubble.ts +1 -1
  82. package/src/components/widget-view.test.ts +181 -0
  83. package/src/components/widget-view.ts +234 -0
  84. package/src/defaults.ts +54 -51
  85. package/src/generated/runtype-openapi-contract.ts +5 -0
  86. package/src/index-core.ts +30 -4
  87. package/src/index-global.ts +73 -6
  88. package/src/index.ts +7 -1
  89. package/src/install.test.ts +11 -11
  90. package/src/install.ts +28 -18
  91. package/src/launcher-global.ts +25 -14
  92. package/src/markdown-parsers-eager.ts +20 -0
  93. package/src/markdown-parsers-entry.ts +4 -0
  94. package/src/markdown-parsers-loader.ts +49 -0
  95. package/src/plugin-kit.test.ts +1 -1
  96. package/src/plugin-kit.ts +7 -7
  97. package/src/plugins/types.ts +5 -5
  98. package/src/postprocessors.ts +27 -14
  99. package/src/runtime/host-layout.test.ts +1 -1
  100. package/src/runtime/host-layout.ts +4 -5
  101. package/src/runtime/init.ts +2 -2
  102. package/src/session.test.ts +5 -6
  103. package/src/session.ts +333 -227
  104. package/src/session.voice.test.ts +146 -0
  105. package/src/session.webmcp.test.ts +24 -25
  106. package/src/smart-dom-reader.ts +3 -4
  107. package/src/styles/widget.css +54 -36
  108. package/src/suggest-replies-tool.test.ts +3 -3
  109. package/src/suggest-replies-tool.ts +5 -5
  110. package/src/testing/index.ts +1 -1
  111. package/src/theme-editor/color-utils.ts +2 -2
  112. package/src/theme-editor/index.ts +4 -13
  113. package/src/theme-editor/presets.ts +1 -1
  114. package/src/theme-editor/preview-utils.ts +22 -20
  115. package/src/theme-editor/preview.ts +4 -1
  116. package/src/theme-editor/role-mappings.ts +3 -3
  117. package/src/theme-editor/sections.ts +21 -20
  118. package/src/theme-editor/types.ts +1 -1
  119. package/src/theme-editor/webmcp/index.ts +1 -1
  120. package/src/theme-editor/webmcp/summary.ts +1 -2
  121. package/src/theme-editor/webmcp/tools.ts +30 -30
  122. package/src/theme-editor/webmcp/types.ts +2 -2
  123. package/src/theme-editor-preview.ts +10 -0
  124. package/src/theme-reference.ts +26 -26
  125. package/src/types.ts +368 -99
  126. package/src/ui.approval-plugin.test.ts +3 -3
  127. package/src/ui.ask-user-question-plugin.test.ts +9 -9
  128. package/src/ui.component-directive.test.ts +4 -4
  129. package/src/ui.composer-bar.test.ts +10 -11
  130. package/src/ui.composer-keyboard.test.ts +5 -5
  131. package/src/ui.header-icon-color.test.ts +59 -0
  132. package/src/ui.scroll.test.ts +5 -6
  133. package/src/ui.stop-button.test.ts +3 -4
  134. package/src/ui.suggest-replies.test.ts +2 -2
  135. package/src/ui.ts +292 -131
  136. package/src/utils/buttons.ts +1 -1
  137. package/src/utils/code-generators.ts +1 -1
  138. package/src/utils/composer-history.ts +3 -3
  139. package/src/utils/dom-context.test.ts +2 -2
  140. package/src/utils/dom-context.ts +7 -7
  141. package/src/utils/dom.test.ts +67 -0
  142. package/src/utils/dom.ts +75 -0
  143. package/src/utils/dropdown.ts +2 -2
  144. package/src/utils/event-stream-buffer.test.ts +2 -2
  145. package/src/utils/message-fingerprint.test.ts +9 -0
  146. package/src/utils/message-fingerprint.ts +5 -0
  147. package/src/utils/morph.ts +1 -1
  148. package/src/utils/sanitize.ts +32 -16
  149. package/src/utils/sequence-buffer.test.ts +8 -8
  150. package/src/utils/sequence-buffer.ts +4 -4
  151. package/src/utils/smart-dom-adapter.test.ts +1 -1
  152. package/src/utils/smart-dom-adapter.ts +4 -5
  153. package/src/utils/speech-text.test.ts +122 -0
  154. package/src/utils/speech-text.ts +101 -0
  155. package/src/utils/stream-animation.test.ts +3 -3
  156. package/src/utils/stream-animation.ts +6 -6
  157. package/src/utils/theme.test.ts +1 -1
  158. package/src/utils/throughput-tracker.test.ts +11 -11
  159. package/src/utils/throughput-tracker.ts +9 -9
  160. package/src/utils/tokens.ts +13 -13
  161. package/src/utils/virtual-scroller.ts +1 -1
  162. package/src/vendor/smart-dom-reader/README.md +4 -4
  163. package/src/vendor/smart-dom-reader/index.d.ts +1 -1
  164. package/src/vendor/smart-dom-reader/index.js +2 -2
  165. package/src/version.ts +1 -1
  166. package/src/voice/audio-playback-manager.test.ts +159 -0
  167. package/src/voice/audio-playback-manager.ts +145 -30
  168. package/src/voice/browser-speech-engine.ts +130 -0
  169. package/src/voice/fallback-speech-engine.ts +89 -0
  170. package/src/voice/index.ts +21 -0
  171. package/src/voice/read-aloud-controller.test.ts +143 -0
  172. package/src/voice/read-aloud-controller.ts +136 -0
  173. package/src/voice/runtype-speech-engine.test.ts +280 -0
  174. package/src/voice/runtype-speech-engine.ts +211 -0
  175. package/src/voice/runtype-tts-entry.ts +10 -0
  176. package/src/voice/runtype-tts-loader.test.ts +47 -0
  177. package/src/voice/runtype-tts-loader.ts +42 -0
  178. package/src/voice/runtype-voice-provider.ts +332 -537
  179. package/src/voice/voice-factory.ts +26 -4
  180. package/src/voice/voice.test.ts +321 -7
  181. package/src/voice/worklet-playback-engine.ts +272 -0
  182. package/src/voice-worklet-player.ts +39 -0
  183. package/src/webmcp-bridge.test.ts +6 -6
  184. package/src/webmcp-bridge.ts +24 -25
  185. package/src/webmcp-polyfill.ts +1 -1
  186. package/src/voice/voice-activity-detector.ts +0 -90
@@ -0,0 +1,211 @@
1
+ // Runtype Speech Engine (streaming TTS)
2
+ //
3
+ // Built-in `SpeechEngine` that backs the per-message "Read aloud" action (and
4
+ // the auto-speak path) with Runtype-hosted text-to-speech. It is the client
5
+ // half of the "Option A" HTTP synthesize design: a stateless
6
+ //
7
+ // POST {host}/v1/agents/:agentId/speak -> streamed PCM16 / 24 kHz / mono
8
+ //
9
+ // The `clientToken` is browser-safe (same one the chat widget uses, scoped by
10
+ // `allowedOrigins`), so this calls Runtype directly from the page — no proxy.
11
+ // The streamed PCM is fed chunk-by-chunk into a {@link PcmStreamPlayer}, so this
12
+ // engine stays tiny: fetch -> enqueue chunks -> markStreamEnd. The player owns
13
+ // prebuffering, gapless scheduling, graceful underrun, and pause/resume.
14
+ //
15
+ // By default the player is the in-bundle, main-thread `AudioPlaybackManager`
16
+ // (no AudioWorklet module — keeps the main bundle lean). Consumers who want the
17
+ // jitter-buffered AudioWorklet player inject `createPcmStreamPlayer` from
18
+ // `@runtypelabs/persona/voice-worklet-player` via `createPlaybackEngine` (config:
19
+ // `textToSpeech.createPlaybackEngine`); the worklet then lands in their bundle.
20
+ //
21
+ // Wired automatically by `textToSpeech: { provider: 'runtype' }` (see
22
+ // `session.ts`), which derives `host`/`agentId`/`clientToken` from the widget
23
+ // config and — unless `browserFallback: false` — wraps this in a
24
+ // `FallbackSpeechEngine` so a missing endpoint or transient failure falls back
25
+ // to the browser voice instead of erroring.
26
+
27
+ import type {
28
+ PcmStreamPlayer,
29
+ SpeechCallbacks,
30
+ SpeechEngine,
31
+ SpeechRequest,
32
+ } from "../types";
33
+ import { AudioPlaybackManager } from "./audio-playback-manager";
34
+
35
+ export interface RuntypeSpeechEngineOptions {
36
+ /**
37
+ * Runtype API host, e.g. `https://api.runtype.com` (typically the widget's
38
+ * `apiUrl`). A trailing slash is tolerated.
39
+ */
40
+ host: string;
41
+ /** Agent whose configured voice synthesizes the text. */
42
+ agentId: string;
43
+ /** Browser-safe client token — the same one the chat widget uses. */
44
+ clientToken: string;
45
+ /**
46
+ * Default voice id, used when a `SpeechRequest` doesn't carry its own. When
47
+ * omitted the agent's configured voice is used.
48
+ */
49
+ voice?: string;
50
+ /**
51
+ * Audio (ms) the player buffers before the first sample and after an
52
+ * underrun. Runtype streams steadily, so the default (200) keeps first sound
53
+ * close to time-to-first-byte while still riding out small hiccups. Applies to
54
+ * the default {@link AudioPlaybackManager}; a custom `createPlaybackEngine` is
55
+ * responsible for its own prebuffer.
56
+ */
57
+ prebufferMs?: number;
58
+ /**
59
+ * Factory for the streaming PCM player. Defaults to the in-bundle, main-thread
60
+ * {@link AudioPlaybackManager} (with `prebufferMs`). Pass `createPcmStreamPlayer`
61
+ * from `@runtypelabs/persona/voice-worklet-player` for the jitter-buffered
62
+ * AudioWorklet player (it then ships in your bundle, not Persona's). May be
63
+ * async — it is resolved on first playback, inside the user gesture.
64
+ */
65
+ createPlaybackEngine?: () => PcmStreamPlayer | Promise<PcmStreamPlayer>;
66
+ /**
67
+ * Optional hook for surfacing fetch/stream failures (a missing endpoint, an
68
+ * expired token, an upstream 4xx) to a log or telemetry. The widget itself
69
+ * only returns the read-aloud button to idle (or, with a fallback engine,
70
+ * silently switches to the browser voice), so without this the reason is
71
+ * invisible.
72
+ */
73
+ onError?: (error: Error) => void;
74
+ }
75
+
76
+ /** Strip a trailing slash so `${host}/v1/...` never doubles up. */
77
+ function normalizeHost(host: string): string {
78
+ return host.replace(/\/+$/, "");
79
+ }
80
+
81
+ /** Streaming `SpeechEngine` backed by Runtype's `/v1/agents/:id/speak`. */
82
+ export class RuntypeSpeechEngine implements SpeechEngine {
83
+ readonly id = "runtype-tts";
84
+ // The PCM player pauses/resumes via AudioContext.suspend() — solid, unlike
85
+ // speechSynthesis.pause().
86
+ readonly supportsPause = true;
87
+
88
+ private player: PcmStreamPlayer | null = null;
89
+ private playerPromise: Promise<PcmStreamPlayer> | null = null;
90
+ // Bumped on every speak()/stop() so a superseded request's async callbacks and
91
+ // its in-flight stream read loop become no-ops.
92
+ private generation = 0;
93
+
94
+ constructor(private readonly opts: RuntypeSpeechEngineOptions) {}
95
+
96
+ // Create one player lazily (a worklet engine's addModule is async), then reuse
97
+ // it across speaks; flush() between replies clears the queue without tearing
98
+ // down the AudioContext. Defaults to the in-bundle AudioPlaybackManager.
99
+ private ensurePlayer(): Promise<PcmStreamPlayer> {
100
+ return (this.playerPromise ??= Promise.resolve(
101
+ this.opts.createPlaybackEngine
102
+ ? this.opts.createPlaybackEngine()
103
+ : new AudioPlaybackManager(24000, {
104
+ prebufferMs: this.opts.prebufferMs ?? 200,
105
+ }),
106
+ ).then((player) => (this.player = player)));
107
+ }
108
+
109
+ speak(request: SpeechRequest, callbacks: SpeechCallbacks): void {
110
+ const gen = ++this.generation;
111
+ // Run the async fetch/stream without making speak() itself async — the
112
+ // widget surfaces the time until audio starts as the "loading" state.
113
+ void this.run(gen, request, callbacks);
114
+ }
115
+
116
+ private async run(
117
+ gen: number,
118
+ request: SpeechRequest,
119
+ callbacks: SpeechCallbacks,
120
+ ): Promise<void> {
121
+ try {
122
+ const player = await this.ensurePlayer();
123
+ if (gen !== this.generation) return; // superseded while the worklet booted
124
+ player.flush(); // drop any prior playback (and its callbacks)
125
+ player.resume(); // clear a prior pause so this reply isn't stuck suspended
126
+
127
+ // Drive read-aloud state from the player, not from chunk arrival: onStarted
128
+ // fires when the prebuffer fills and audio is actually audible (loading ->
129
+ // playing); onFinished when it drains (-> idle). An empty stream produces
130
+ // no onStarted and an immediate onFinished, so the UI skips straight back
131
+ // to idle without a phantom "playing".
132
+ player.onStarted(() => {
133
+ if (gen === this.generation) callbacks.onStart?.();
134
+ });
135
+ player.onFinished(() => {
136
+ if (gen === this.generation) callbacks.onEnd?.();
137
+ });
138
+
139
+ const url = `${normalizeHost(this.opts.host)}/v1/agents/${encodeURIComponent(
140
+ this.opts.agentId,
141
+ )}/speak`;
142
+ const res = await fetch(url, {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ // Match Runtype's client-token auth convention. Never placed in the
147
+ // URL/query string.
148
+ Authorization: `Bearer ${this.opts.clientToken}`,
149
+ },
150
+ body: JSON.stringify({
151
+ text: request.text,
152
+ voice: request.voice ?? this.opts.voice,
153
+ format: "pcm",
154
+ }),
155
+ });
156
+ if (gen !== this.generation) return; // superseded while awaiting headers
157
+ if (!res.ok || !res.body) throw new Error(await describeError(res));
158
+
159
+ const reader = res.body.getReader();
160
+ for (;;) {
161
+ const { done, value } = await reader.read();
162
+ if (gen !== this.generation) {
163
+ // A newer speak()/stop() won — stop pulling and release the stream.
164
+ await reader.cancel().catch(() => {});
165
+ return;
166
+ }
167
+ if (done) break;
168
+ if (value && value.byteLength > 0) player.enqueue(value);
169
+ }
170
+
171
+ player.markStreamEnd();
172
+ } catch (err) {
173
+ if (gen !== this.generation) return; // error from a superseded request
174
+ const error = err instanceof Error ? err : new Error(String(err));
175
+ this.opts.onError?.(error); // surface the reason (log, telemetry, …)
176
+ callbacks.onError?.(error); // and let the widget (or fallback) react
177
+ }
178
+ }
179
+
180
+ pause(): void {
181
+ this.player?.pause();
182
+ }
183
+
184
+ resume(): void {
185
+ this.player?.resume();
186
+ }
187
+
188
+ stop(): void {
189
+ this.generation++; // invalidate any in-flight stream + pending onFinished
190
+ this.player?.flush();
191
+ }
192
+
193
+ destroy(): void {
194
+ this.generation++;
195
+ void this.player?.destroy();
196
+ this.player = null;
197
+ this.playerPromise = null;
198
+ }
199
+ }
200
+
201
+ /** Best-effort human-readable message from a non-OK speak response. */
202
+ async function describeError(res: Response): Promise<string> {
203
+ try {
204
+ const data = (await res.json()) as { error?: string; detail?: string };
205
+ return data.detail
206
+ ? `${data.error ?? `Runtype TTS ${res.status}`}: ${data.detail}`
207
+ : data.error ?? `Runtype TTS request failed (${res.status})`;
208
+ } catch {
209
+ return `Runtype TTS request failed (${res.status})`;
210
+ }
211
+ }
@@ -0,0 +1,10 @@
1
+ // Standalone entry for the deferred Runtype TTS chunk (`dist/runtype-tts.js`).
2
+ //
3
+ // Bundles the hosted read-aloud engine, its browser-fallback wrapper, and the
4
+ // `AudioPlaybackManager` they depend on, so the IIFE/CDN build can load the
5
+ // whole `provider: 'runtype'` read-aloud path on demand (kept out of
6
+ // `index.global.js`). `session.ts` resolves this module via
7
+ // `runtype-tts-loader.ts`; the loader is overridden in `index-global.ts` to
8
+ // fetch this chunk from a sibling URL. See `runtype-tts-loader.ts`.
9
+ export { RuntypeSpeechEngine } from "./runtype-speech-engine";
10
+ export { FallbackSpeechEngine } from "./fallback-speech-engine";
@@ -0,0 +1,47 @@
1
+ // Tests for the deferred Runtype TTS loader indirection — the seam the IIFE/CDN
2
+ // build uses to fetch the standalone `runtype-tts.js` chunk instead of inlining
3
+ // the engine. (See runtype-tts-loader.ts / index-global.ts.)
4
+ import { describe, it, expect, afterEach, vi } from "vitest";
5
+ import {
6
+ setRuntypeTtsLoader,
7
+ loadRuntypeTts,
8
+ type RuntypeTtsModule,
9
+ } from "./runtype-tts-loader";
10
+
11
+ afterEach(() => {
12
+ setRuntypeTtsLoader(null); // restore the default (inlined) import
13
+ });
14
+
15
+ describe("runtype-tts-loader", () => {
16
+ it("uses a registered loader when one is set (the IIFE/CDN path)", async () => {
17
+ const fakeModule = {
18
+ RuntypeSpeechEngine: class {},
19
+ FallbackSpeechEngine: class {},
20
+ } as unknown as RuntypeTtsModule;
21
+ const loader = vi.fn(async () => fakeModule);
22
+ setRuntypeTtsLoader(loader);
23
+
24
+ const mod = await loadRuntypeTts();
25
+
26
+ expect(loader).toHaveBeenCalledTimes(1);
27
+ expect(mod).toBe(fakeModule);
28
+ });
29
+
30
+ it("falls back to the inlined import when no loader is registered", async () => {
31
+ setRuntypeTtsLoader(null);
32
+
33
+ // The default path resolves the real chunk entry, which exports both engines.
34
+ const mod = await loadRuntypeTts();
35
+
36
+ expect(typeof mod.RuntypeSpeechEngine).toBe("function");
37
+ expect(typeof mod.FallbackSpeechEngine).toBe("function");
38
+ });
39
+
40
+ it("setRuntypeTtsLoader(null) restores the default after an override", async () => {
41
+ setRuntypeTtsLoader(vi.fn(async () => ({}) as RuntypeTtsModule));
42
+ setRuntypeTtsLoader(null);
43
+
44
+ const mod = await loadRuntypeTts();
45
+ expect(typeof mod.RuntypeSpeechEngine).toBe("function");
46
+ });
47
+ });
@@ -0,0 +1,42 @@
1
+ // Deferred loader for the hosted Runtype TTS read-aloud engine.
2
+ //
3
+ // The engine (`RuntypeSpeechEngine` + the `AudioPlaybackManager` it bundles) is
4
+ // ~4–5 kB and only used when `textToSpeech.provider: 'runtype'` is configured —
5
+ // an opt-in. To keep it out of the CDN payload (`index.global.js`), the IIFE
6
+ // build marks the default `import("./runtype-speech-engine")` below external and
7
+ // `index-global.ts` registers a loader that imports the standalone
8
+ // `runtype-tts.js` chunk from a sibling URL instead. Mirrors how the WebMCP
9
+ // polyfill is deferred (see `setWebMcpPolyfillLoader` in `webmcp-bridge.ts`).
10
+ //
11
+ // In every other build (ESM/CJS main entry, theme-editor) no loader is
12
+ // registered, so the default relative import resolves and the engine is inlined
13
+ // — those bundlers code-split or have headroom, so there's no runtime fetch for
14
+ // npm consumers.
15
+
16
+ import type { RuntypeSpeechEngine } from "./runtype-speech-engine";
17
+ import type { FallbackSpeechEngine } from "./fallback-speech-engine";
18
+
19
+ /** The slice of the engine chunk the session consumes. */
20
+ export type RuntypeTtsModule = {
21
+ RuntypeSpeechEngine: typeof RuntypeSpeechEngine;
22
+ FallbackSpeechEngine: typeof FallbackSpeechEngine;
23
+ };
24
+
25
+ let loader: (() => Promise<RuntypeTtsModule>) | null = null;
26
+
27
+ /**
28
+ * Override how the Runtype TTS engine module is obtained. By default the session
29
+ * does `import("./runtype-tts-entry")`, which bundlers resolve/inline. The
30
+ * IIFE/CDN entry registers a loader that imports the self-contained
31
+ * `runtype-tts.js` chunk from a URL derived from the widget script's own `src`.
32
+ * Pass `null` to restore the default (used by tests).
33
+ */
34
+ export const setRuntypeTtsLoader = (
35
+ l: (() => Promise<RuntypeTtsModule>) | null,
36
+ ): void => {
37
+ loader = l;
38
+ };
39
+
40
+ /** Resolve the Runtype TTS engine module (registered loader, else inlined import). */
41
+ export const loadRuntypeTts = (): Promise<RuntypeTtsModule> =>
42
+ loader ? loader() : import("./runtype-tts-entry");