@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.
- package/README.md +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +12 -12
- package/src/client.ts +30 -31
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- 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");
|