@missionsquad/browserai 0.1.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 (146) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +229 -0
  3. package/dist/audio/clip.d.ts +51 -0
  4. package/dist/audio/clip.d.ts.map +1 -0
  5. package/dist/audio/clip.js +71 -0
  6. package/dist/audio/clip.js.map +1 -0
  7. package/dist/audio/decode.d.ts +7 -0
  8. package/dist/audio/decode.d.ts.map +1 -0
  9. package/dist/audio/decode.js +19 -0
  10. package/dist/audio/decode.js.map +1 -0
  11. package/dist/audio/pcm.d.ts +13 -0
  12. package/dist/audio/pcm.d.ts.map +1 -0
  13. package/dist/audio/pcm.js +66 -0
  14. package/dist/audio/pcm.js.map +1 -0
  15. package/dist/audio/recorder.d.ts +40 -0
  16. package/dist/audio/recorder.d.ts.map +1 -0
  17. package/dist/audio/recorder.js +127 -0
  18. package/dist/audio/recorder.js.map +1 -0
  19. package/dist/audio/streaming-player.d.ts +29 -0
  20. package/dist/audio/streaming-player.d.ts.map +1 -0
  21. package/dist/audio/streaming-player.js +82 -0
  22. package/dist/audio/streaming-player.js.map +1 -0
  23. package/dist/client.d.ts +195 -0
  24. package/dist/client.d.ts.map +1 -0
  25. package/dist/client.js +638 -0
  26. package/dist/client.js.map +1 -0
  27. package/dist/config.d.ts +81 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +19 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/errors.d.ts +52 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +74 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/events.d.ts +18 -0
  36. package/dist/events.d.ts.map +1 -0
  37. package/dist/events.js +43 -0
  38. package/dist/events.js.map +1 -0
  39. package/dist/generation.d.ts +12 -0
  40. package/dist/generation.d.ts.map +1 -0
  41. package/dist/generation.js +30 -0
  42. package/dist/generation.js.map +1 -0
  43. package/dist/hardware.d.ts +23 -0
  44. package/dist/hardware.d.ts.map +1 -0
  45. package/dist/hardware.js +150 -0
  46. package/dist/hardware.js.map +1 -0
  47. package/dist/history.d.ts +81 -0
  48. package/dist/history.d.ts.map +1 -0
  49. package/dist/history.js +193 -0
  50. package/dist/history.js.map +1 -0
  51. package/dist/index.d.ts +51 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +50 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/inference.d.ts +89 -0
  56. package/dist/inference.d.ts.map +1 -0
  57. package/dist/inference.js +411 -0
  58. package/dist/inference.js.map +1 -0
  59. package/dist/json.d.ts +22 -0
  60. package/dist/json.d.ts.map +1 -0
  61. package/dist/json.js +145 -0
  62. package/dist/json.js.map +1 -0
  63. package/dist/metrics.d.ts +18 -0
  64. package/dist/metrics.d.ts.map +1 -0
  65. package/dist/metrics.js +59 -0
  66. package/dist/metrics.js.map +1 -0
  67. package/dist/model-cache.d.ts +49 -0
  68. package/dist/model-cache.d.ts.map +1 -0
  69. package/dist/model-cache.js +511 -0
  70. package/dist/model-cache.js.map +1 -0
  71. package/dist/model-source.d.ts +27 -0
  72. package/dist/model-source.d.ts.map +1 -0
  73. package/dist/model-source.js +110 -0
  74. package/dist/model-source.js.map +1 -0
  75. package/dist/models.d.ts +103 -0
  76. package/dist/models.d.ts.map +1 -0
  77. package/dist/models.js +1052 -0
  78. package/dist/models.js.map +1 -0
  79. package/dist/multimodal.d.ts +261 -0
  80. package/dist/multimodal.d.ts.map +1 -0
  81. package/dist/multimodal.js +506 -0
  82. package/dist/multimodal.js.map +1 -0
  83. package/dist/prompt.d.ts +13 -0
  84. package/dist/prompt.d.ts.map +1 -0
  85. package/dist/prompt.js +32 -0
  86. package/dist/prompt.js.map +1 -0
  87. package/dist/proxy-verify.d.ts +10 -0
  88. package/dist/proxy-verify.d.ts.map +1 -0
  89. package/dist/proxy-verify.js +98 -0
  90. package/dist/proxy-verify.js.map +1 -0
  91. package/dist/schema-validator.d.ts +6 -0
  92. package/dist/schema-validator.d.ts.map +1 -0
  93. package/dist/schema-validator.js +104 -0
  94. package/dist/schema-validator.js.map +1 -0
  95. package/dist/schema.d.ts +98 -0
  96. package/dist/schema.d.ts.map +1 -0
  97. package/dist/schema.js +115 -0
  98. package/dist/schema.js.map +1 -0
  99. package/dist/text-utils.d.ts +8 -0
  100. package/dist/text-utils.d.ts.map +1 -0
  101. package/dist/text-utils.js +14 -0
  102. package/dist/text-utils.js.map +1 -0
  103. package/dist/voice/helpers.d.ts +35 -0
  104. package/dist/voice/helpers.d.ts.map +1 -0
  105. package/dist/voice/helpers.js +49 -0
  106. package/dist/voice/helpers.js.map +1 -0
  107. package/dist/voice/voice-pipeline.d.ts +155 -0
  108. package/dist/voice/voice-pipeline.d.ts.map +1 -0
  109. package/dist/voice/voice-pipeline.js +407 -0
  110. package/dist/voice/voice-pipeline.js.map +1 -0
  111. package/dist/webllm.worker.d.ts +2 -0
  112. package/dist/webllm.worker.d.ts.map +1 -0
  113. package/dist/webllm.worker.js +8 -0
  114. package/dist/webllm.worker.js.map +1 -0
  115. package/package.json +62 -0
  116. package/src/audio/clip.ts +94 -0
  117. package/src/audio/decode.ts +19 -0
  118. package/src/audio/pcm.ts +62 -0
  119. package/src/audio/recorder.ts +146 -0
  120. package/src/audio/streaming-player.ts +99 -0
  121. package/src/client.ts +784 -0
  122. package/src/config.ts +104 -0
  123. package/src/errors.ts +81 -0
  124. package/src/events.ts +53 -0
  125. package/src/generation.ts +39 -0
  126. package/src/hardware.ts +193 -0
  127. package/src/history.ts +257 -0
  128. package/src/index.ts +203 -0
  129. package/src/inference.ts +579 -0
  130. package/src/json.ts +169 -0
  131. package/src/metrics.ts +63 -0
  132. package/src/model-cache.ts +637 -0
  133. package/src/model-source.ts +142 -0
  134. package/src/models.ts +1171 -0
  135. package/src/multimodal.ts +697 -0
  136. package/src/prompt.ts +33 -0
  137. package/src/proxy-verify.ts +113 -0
  138. package/src/schema-validator.ts +117 -0
  139. package/src/schema.ts +118 -0
  140. package/src/text-utils.ts +12 -0
  141. package/src/voice/helpers.ts +59 -0
  142. package/src/voice/voice-pipeline.ts +513 -0
  143. package/src/webllm.worker.ts +9 -0
  144. package/worker-template/README.md +39 -0
  145. package/worker-template/cloudflare-worker.ts +389 -0
  146. package/worker-template/wrangler.jsonc +19 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Mission Squad
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # @missionsquad/browserai
2
+
3
+ Run small open AI models **entirely in the browser** with WebGPU — no inference server — as an
4
+ embeddable, UI-free TypeScript SDK. BrowserAI packages a curated 47-model catalog, model
5
+ loading/downloading with progress, capability-slot management, three inference runtimes, streaming,
6
+ schema-constrained JSON generation, run metrics, browser-cache management, and a complete
7
+ press-to-talk voice-assistant pipeline. Drop it into any frontend site and build your own UI on top.
8
+
9
+ Three inference runtimes:
10
+
11
+ - **WebLLM (MLC)** for quantized MLC text models, with schema-constrained JSON decoding and detailed
12
+ per-token runtime telemetry.
13
+ - **Transformers.js + ONNX/WebGPU** for browser-ready ONNX models: text LLMs, Whisper/Moonshine/
14
+ Parakeet transcription, Supertonic speech, SmolVLM/Qwen-VL/OCR vision, Gemma 4 multimodal, and
15
+ audio-LLM ASR (Granite Speech, Voxtral realtime).
16
+ - **kokoro-js** (loaded from its CDN bundle by default) for Kokoro 82M text-to-speech, including
17
+ sentence-by-sentence streaming synthesis.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @missionsquad/browserai
23
+ ```
24
+
25
+ Requirements: a WebGPU-capable browser (lightweight speech models — Whisper/Moonshine/Parakeet
26
+ transcription and the TTS engines — can fall back to wasm; text, vision, and audio-LLM models
27
+ require WebGPU), Node.js 20+ for development. `@mlc-ai/web-llm` and `@huggingface/transformers` are regular
28
+ dependencies, dynamically imported only when a model that needs them loads — your initial bundle
29
+ stays small.
30
+
31
+ ## Quickstart
32
+
33
+ ```ts
34
+ import { BrowserAI } from "@missionsquad/browserai";
35
+
36
+ const ai = new BrowserAI();
37
+ ai.on("loadprogress", ({ progress, status }) => console.log(progress, status));
38
+
39
+ await ai.load("Qwen3.5-0.8B-q4f16_1-MLC"); // downloads once, then serves from browser cache
40
+
41
+ const { text, stats } = await ai.generateText(
42
+ [
43
+ { role: "system", content: "You are terse." },
44
+ { role: "user", content: "Why is the sky blue?" },
45
+ ],
46
+ { runtime: { jsonMode: "none" }, onDelta: (full) => render(full) },
47
+ );
48
+ console.log(text, stats.extra?.decode_tokens_per_s);
49
+ ```
50
+
51
+ ### Capability slots
52
+
53
+ Models load into one of four **capability slots** — `text`, `vision`, `stt` (transcription), `tts`
54
+ (speech) — with at most one model per slot, so up to four models can be resident at once. Loading a
55
+ model whose slots are occupied replaces the conflicting owner. Loads/unloads are single-flight, and
56
+ runs are serialized per loaded model, so concurrent calls queue instead of driving one engine from
57
+ two places.
58
+
59
+ ```ts
60
+ await ai.load("onnx-community/whisper-base"); // stt slot
61
+ await ai.load("onnx-community/Kokoro-82M-v1.0-ONNX"); // tts slot
62
+
63
+ const { text } = await ai.transcribe(pcm16k); // Float32Array @ 16 kHz mono
64
+ const clip = await ai.synthesizeToClip("Hello!", { voice: "af_heart" });
65
+ ai.occupiedSlots(); // Set { "text", "stt", "tts" }
66
+ ```
67
+
68
+ ### Schema-constrained JSON
69
+
70
+ ```ts
71
+ import { buildExtractionMessages, parseJsonFromModel, validateAgainstJsonSchema } from "@missionsquad/browserai";
72
+
73
+ const messages = buildExtractionMessages(inputText, mySchema);
74
+ const { text } = await ai.generateText(messages, { schema: mySchema }); // WebLLM: real constrained decoding
75
+ const parsed = parseJsonFromModel(text);
76
+ if (parsed.ok) console.log(validateAgainstJsonSchema(parsed.value, mySchema)); // [] when valid
77
+ ```
78
+
79
+ WebLLM backends enforce the schema with a grammar-constrained decoder; Transformers.js backends are
80
+ prompt-guided, so validate the output either way.
81
+
82
+ ### Voice pipeline (press-to-talk assistant)
83
+
84
+ `VoicePipeline` chains mic → STT → text LLM → TTS → speakers as a headless state machine with typed
85
+ events. With Kokoro in the speech slot, replies are spoken **while the text model is still
86
+ generating** (audio starts after the first sentence); other TTS models speak the finished reply as
87
+ one clip. Pressing talk during playback stops it (barge-in).
88
+
89
+ ```ts
90
+ import { BrowserAI, VoicePipeline } from "@missionsquad/browserai";
91
+
92
+ const ai = new BrowserAI();
93
+ const voice = new VoicePipeline(ai);
94
+
95
+ await voice.loadRecommendedModels(); // Whisper Base + Qwen3.5 0.8B + Kokoro 82M (~2.5 GB VRAM)
96
+
97
+ voice.on("stagechange", ({ stage }) => setIndicator(stage)); // idle/listening/transcribing/thinking/speaking/error
98
+ voice.on("transcript", ({ text }) => addUserBubble(text));
99
+ voice.on("replydelta", ({ text }) => updateAssistantBubble(text));
100
+ voice.on("turncommitted", ({ messages }) => renderHistory(messages));
101
+ voice.on("error", ({ label, message }) => showError(`${label}: ${message}`));
102
+
103
+ talkButton.onclick = () => voice.toggle(); // press to talk, press again to send
104
+ newChatButton.onclick = () => voice.reset();
105
+ ```
106
+
107
+ The turn commits to history only on success; discarded takes (`turndiscarded`: too short, no speech,
108
+ empty reply, contention with other work) leave history untouched. For input-level visualization,
109
+ subscribe to `micstream` (attach an AnalyserNode) and `micstreamended`; for output, pass
110
+ `streamingAudio.onGraphReady` to tap the streaming player's gain node.
111
+
112
+ ### Vision
113
+
114
+ ```ts
115
+ await ai.load("HuggingFaceTB/SmolVLM-256M-Instruct");
116
+ const answer = await ai.describeImage(imageDataUrl, "What does this chart show?", (partial) => render(partial));
117
+ ```
118
+
119
+ ### Metrics + run history
120
+
121
+ Every generation returns `RuntimeStats` (tokens, decode tok/s, TTFT, per-token latency breakdown for
122
+ WebLLM, measured telemetry for Transformers.js). Persist and aggregate runs with `RunHistoryStore`:
123
+
124
+ ```ts
125
+ import { RunHistoryStore, runRecordFromStats } from "@missionsquad/browserai";
126
+
127
+ const history = new RunHistoryStore(); // localStorage-backed; pass { storage: null } for in-memory
128
+ const record = runRecordFromStats("chat", stats);
129
+ if (record) history.append(record);
130
+ history.aggregateByModel("chat"); // decode/ttft averages per model
131
+ ```
132
+
133
+ ### Cache management
134
+
135
+ Models download once (hundreds of MB to a few GB) and are cached in browser storage — WebLLM weights
136
+ in IndexedDB by default (configurable via `cacheBackend`), Transformers.js artifacts always in the
137
+ Cache API bucket `transformers-cache`.
138
+
139
+ ```ts
140
+ await ai.cacheStatus("onnx-community/whisper-base"); // { downloaded, matchedEntries, … }
141
+ await ai.deleteModelArtifacts("onnx-community/whisper-base");
142
+ await ai.deleteAllModelArtifacts(); // releases loaded models first
143
+ await ai.estimateStorage(); // origin-wide usage/quota
144
+ ```
145
+
146
+ Cleanup targets model artifact storage only — never cookies, localStorage, or unrelated site data.
147
+
148
+ ## Configuration
149
+
150
+ ```ts
151
+ const ai = new BrowserAI({
152
+ modelSource: "direct", // "direct" (default, straight from Hugging Face) | "proxy"
153
+ proxyOrigin: undefined, // proxy Worker origin; defaults to the page origin in proxy mode
154
+ verifyProxy: true, // probe the proxy before each proxied load
155
+ cacheBackend: "indexeddb", // WebLLM artifact cache: "indexeddb" | "cache" | "opfs" | "cross-origin"
156
+ webllm: {
157
+ worker: () => new Worker(new URL("./llm.worker.ts", import.meta.url), { type: "module" }),
158
+ logLevel: "INFO",
159
+ },
160
+ kokoro: { url: "https://cdn.jsdelivr.net/npm/kokoro-js@1.2.1/+esm" }, // or load: () => import("kokoro-js")
161
+ tts: { voice: "af_heart", speed: 1, pitch: 1 },
162
+ history: { key: "browserai:v1:history", maxRecords: 200 },
163
+ });
164
+ ```
165
+
166
+ ### Running WebLLM in a worker
167
+
168
+ By default WebLLM engines run on the main thread. To keep loading and token generation off it,
169
+ create a two-line worker entry in your app and pass its factory:
170
+
171
+ ```ts
172
+ // llm.worker.ts — bundled by YOUR build (Vite/webpack understand this pattern)
173
+ export * from "@missionsquad/browserai/worker";
174
+ ```
175
+
176
+ ```ts
177
+ const ai = new BrowserAI({
178
+ webllm: { worker: () => new Worker(new URL("./llm.worker.ts", import.meta.url), { type: "module" }) },
179
+ });
180
+ ```
181
+
182
+ ### Model-artifact proxy (optional)
183
+
184
+ Direct downloads work on any site. If a hosted deployment hits CORS or cache-storage edge cases with
185
+ cross-origin model downloads, deploy the Cloudflare Worker in [`worker-template/`](worker-template/)
186
+ and configure `{ modelSource: "proxy" }`. The Worker proxies `/hf/*` (WebLLM weights),
187
+ `/hf-transformers/*` (Transformers.js artifacts), and `/gh-raw/*` (WebLLM wasm libraries), restricted
188
+ to an allowlist derived from the SDK's model catalog. Kokoro is the one exception: kokoro-js ships
189
+ its own runtime and fetches its weights directly.
190
+
191
+ ## Model catalog
192
+
193
+ 47 presets across four capability types (see `MODEL_PRESETS`; each entry carries GPU-memory
194
+ estimates, download size, stability rating, and per-model default runtime parameters):
195
+
196
+ - **Text (WebLLM/MLC):** Qwen3.5 0.8B/2B/4B, Gemma 3 1B, Gemma 4 E2B/E4B (experimental custom
197
+ records), Llama 3.2 1B/3B, Hermes 3, OLMo 2, Phi-4 mini, Ministral 3, Mistral 7B.
198
+ - **Text (Transformers.js/ONNX):** Nemotron 3 Nano, LFM2.5 230M/350M/1.2B-Thinking, Falcon H1 Tiny,
199
+ the Granite 4.0 family, MiniCPM5, SmolLM3, Gemma 4 E2B/E4B ONNX (multimodal: text + image + audio).
200
+ - **Transcription:** Whisper Base/Tiny/Medium/Medium-EN, Moonshine Base/Tiny, Cohere Transcribe,
201
+ Parakeet CTC 0.6B, Granite Speech 2B, Voxtral Mini 4B Realtime.
202
+ - **Speech:** Kokoro 82M (streams), Supertonic 1/2.
203
+ - **Vision:** SmolVLM 256M/500M, Qwen3-VL 2B, Qwen2.5-VL 3B, GLM-OCR, LightOnOCR-2.
204
+
205
+ ## Browser constraints worth knowing
206
+
207
+ - **WebGPU** is required for text, vision, Gemma-multimodal, and audio-LLM models; ASR-pipeline
208
+ (Whisper/Moonshine/Parakeet) and TTS presets fall back to wasm. `probeHardware()` reports
209
+ adapter/features/limits; other loads throw `WebGPUUnavailableError` without it.
210
+ - `decodeAudioTo16kMono`, `capSrc` (image downscaling), `MicRecorder`, and the audio players use
211
+ main-thread browser APIs (`AudioContext`, `Image`, canvas, `MediaRecorder`) — decode/capture on
212
+ the page even if you move inference elsewhere.
213
+ - If you attach an analyzer to an `<audio>` element playing clips, remember a media element allows
214
+ exactly **one** `MediaElementSourceNode` for its whole life — create the analyzer once and never
215
+ close its context while the element is in use.
216
+ - All SDK errors are `BrowserAIError` subclasses (`ModelLoadError`, `MicrophoneError`,
217
+ `MissingModelError`, `VoiceTurnError`, …) — branch with `instanceof`.
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ npm ci
223
+ npm run build # type-check + emit dist (ESM + .d.ts)
224
+ npm test # vitest unit tests
225
+ ```
226
+
227
+ ## License
228
+
229
+ ISC
@@ -0,0 +1,51 @@
1
+ import type { MultimodalHandle, RawAudio } from "../multimodal.js";
2
+ /** A synthesized, playable speech clip. `samples` is the ORIGINAL (un-pitched) PCM. */
3
+ export type SynthClip = {
4
+ /** Object URL for the (pitch-baked) blob; revoke it when replacing the clip. Absent when `createUrl: false`. */
5
+ url: string | null;
6
+ blob: Blob;
7
+ /** Original un-pitched PCM, kept so a pitch change can re-render without re-running the model. */
8
+ samples: Float32Array;
9
+ sampleRate: number;
10
+ durationSec: number;
11
+ };
12
+ export type SynthesizeClipOptions = {
13
+ voice: string;
14
+ speed?: number;
15
+ /** Pitch ratio baked into the blob (duration-preserving). Default 1 (blob is the model's own output). */
16
+ pitch?: number;
17
+ /** Create an object URL for the blob (default true). Pass false when only PCM/Blob is needed. */
18
+ createUrl?: boolean;
19
+ };
20
+ /**
21
+ * Synthesize `text` with a loaded TTS handle and package it as a playable clip. The pitch shift is
22
+ * baked into the blob (duration-preserving) while `samples` keeps the original PCM so a pitch slider
23
+ * can re-shift without re-synthesis (see {@link repitchClip}).
24
+ */
25
+ export declare function synthesizeClip(handle: MultimodalHandle, text: string, options: SynthesizeClipOptions): Promise<SynthClip>;
26
+ /** Package an already-synthesized RawAudio as a clip (same pitch-baking rules as synthesizeClip). */
27
+ export declare function clipFromRawAudio(audio: RawAudio, pitch: number, createUrl?: boolean): SynthClip;
28
+ /**
29
+ * Re-render a clip from its stored original PCM at a new pitch, revoking the previous object URL.
30
+ * Lets a pitch control take effect immediately on an existing clip without re-running the model.
31
+ */
32
+ export declare function repitchClip(clip: SynthClip, pitch: number): SynthClip;
33
+ /** Revoke a clip's object URL (call when discarding or replacing a clip). Safe on URL-less clips. */
34
+ export declare function releaseClip(clip: SynthClip | null | undefined): void;
35
+ /**
36
+ * Playback rate that applies the speed setting exactly once per engine: Kokoro bakes speed into
37
+ * synthesis (the audio is already time-scaled), so it plays at 1×; the Supertonic pipeline ignores
38
+ * generation-time speed, so the speed setting is applied at playback instead.
39
+ */
40
+ export declare function clipPlaybackRate(handle: MultimodalHandle | null | undefined, speed: number): number;
41
+ /**
42
+ * Play a clip through an HTMLAudioElement. Resolves when playback STARTS (autoplay rejection is
43
+ * swallowed), not when it ends — await the element's "ended" event for drain semantics.
44
+ *
45
+ * Visualization caveat for hosts: a media element can be captured by exactly ONE
46
+ * MediaElementAudioSourceNode for its entire life, and closing the AudioContext that source routes
47
+ * through mutes the element permanently. If you attach an analyzer to `audioEl`, create it once per
48
+ * element and never dispose it (or its context) while the element is still in use.
49
+ */
50
+ export declare function playClipThroughElement(audioEl: HTMLAudioElement, clip: SynthClip, playbackRate?: number): Promise<void>;
51
+ //# sourceMappingURL=clip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/audio/clip.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAInE,uFAAuF;AACvF,MAAM,MAAM,SAAS,GAAG;IACtB,gHAAgH;IAChH,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,kGAAkG;IAClG,OAAO,EAAE,YAAY,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC,CAG/H;AAED,qGAAqG;AACrG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,UAAO,GAAG,SAAS,CAY5F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAUrE;AAED,qGAAqG;AACrG,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnG;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxH"}
@@ -0,0 +1,71 @@
1
+ import { synthesize } from "../multimodal.js";
2
+ import { encodeWavBlob, pitchShiftBuffer } from "./pcm.js";
3
+ /**
4
+ * Synthesize `text` with a loaded TTS handle and package it as a playable clip. The pitch shift is
5
+ * baked into the blob (duration-preserving) while `samples` keeps the original PCM so a pitch slider
6
+ * can re-shift without re-synthesis (see {@link repitchClip}).
7
+ */
8
+ export async function synthesizeClip(handle, text, options) {
9
+ const audio = await synthesize(handle, text, { voice: options.voice, speed: options.speed ?? 1 });
10
+ return clipFromRawAudio(audio, options.pitch ?? 1, options.createUrl ?? true);
11
+ }
12
+ /** Package an already-synthesized RawAudio as a clip (same pitch-baking rules as synthesizeClip). */
13
+ export function clipFromRawAudio(audio, pitch, createUrl = true) {
14
+ const samples = audio.audio;
15
+ const sampleRate = audio.sampling_rate;
16
+ const shifted = pitchShiftBuffer(samples, pitch);
17
+ const blob = pitch === 1 ? audio.toBlob() : encodeWavBlob(shifted, sampleRate);
18
+ return {
19
+ url: createUrl ? URL.createObjectURL(blob) : null,
20
+ blob,
21
+ samples,
22
+ sampleRate,
23
+ durationSec: shifted.length / sampleRate,
24
+ };
25
+ }
26
+ /**
27
+ * Re-render a clip from its stored original PCM at a new pitch, revoking the previous object URL.
28
+ * Lets a pitch control take effect immediately on an existing clip without re-running the model.
29
+ */
30
+ export function repitchClip(clip, pitch) {
31
+ const shifted = pitchShiftBuffer(clip.samples, pitch);
32
+ const blob = encodeWavBlob(shifted, clip.sampleRate);
33
+ if (clip.url)
34
+ URL.revokeObjectURL(clip.url);
35
+ return {
36
+ ...clip,
37
+ url: clip.url === null ? null : URL.createObjectURL(blob),
38
+ blob,
39
+ durationSec: shifted.length / clip.sampleRate,
40
+ };
41
+ }
42
+ /** Revoke a clip's object URL (call when discarding or replacing a clip). Safe on URL-less clips. */
43
+ export function releaseClip(clip) {
44
+ if (clip?.url)
45
+ URL.revokeObjectURL(clip.url);
46
+ }
47
+ /**
48
+ * Playback rate that applies the speed setting exactly once per engine: Kokoro bakes speed into
49
+ * synthesis (the audio is already time-scaled), so it plays at 1×; the Supertonic pipeline ignores
50
+ * generation-time speed, so the speed setting is applied at playback instead.
51
+ */
52
+ export function clipPlaybackRate(handle, speed) {
53
+ return handle?.kind === "tts-kokoro" ? 1 : speed;
54
+ }
55
+ /**
56
+ * Play a clip through an HTMLAudioElement. Resolves when playback STARTS (autoplay rejection is
57
+ * swallowed), not when it ends — await the element's "ended" event for drain semantics.
58
+ *
59
+ * Visualization caveat for hosts: a media element can be captured by exactly ONE
60
+ * MediaElementAudioSourceNode for its entire life, and closing the AudioContext that source routes
61
+ * through mutes the element permanently. If you attach an analyzer to `audioEl`, create it once per
62
+ * element and never dispose it (or its context) while the element is still in use.
63
+ */
64
+ export async function playClipThroughElement(audioEl, clip, playbackRate = 1) {
65
+ if (!clip.url)
66
+ throw new Error("Clip has no object URL (synthesized with createUrl: false).");
67
+ audioEl.src = clip.url;
68
+ audioEl.playbackRate = playbackRate;
69
+ await audioEl.play().catch(() => undefined);
70
+ }
71
+ //# sourceMappingURL=clip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip.js","sourceRoot":"","sources":["../../src/audio/clip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Decode an audio file (ArrayBuffer) to a 16 kHz mono Float32Array, matching Transformers.js
3
+ * `read_audio` output. Browser main-thread only: uses `AudioContext`/`OfflineAudioContext`, which do
4
+ * not exist in workers or Node — decode on the page and pass the PCM into worker-side consumers.
5
+ */
6
+ export declare function decodeAudioTo16kMono(buffer: ArrayBuffer): Promise<Float32Array>;
7
+ //# sourceMappingURL=decode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/audio/decode.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAWrF"}
@@ -0,0 +1,19 @@
1
+ import { AUDIO_SR } from "./pcm.js";
2
+ /**
3
+ * Decode an audio file (ArrayBuffer) to a 16 kHz mono Float32Array, matching Transformers.js
4
+ * `read_audio` output. Browser main-thread only: uses `AudioContext`/`OfflineAudioContext`, which do
5
+ * not exist in workers or Node — decode on the page and pass the PCM into worker-side consumers.
6
+ */
7
+ export async function decodeAudioTo16kMono(buffer) {
8
+ const ctx = new AudioContext();
9
+ // Close the context even when decode rejects (e.g. a header-only blob from an instant start/stop).
10
+ const decoded = await ctx.decodeAudioData(buffer).finally(() => ctx.close().catch(() => undefined));
11
+ const offline = new OfflineAudioContext(1, Math.ceil(decoded.duration * AUDIO_SR), AUDIO_SR);
12
+ const source = offline.createBufferSource();
13
+ source.buffer = decoded;
14
+ source.connect(offline.destination);
15
+ source.start();
16
+ const rendered = await offline.startRendering();
17
+ return rendered.getChannelData(0);
18
+ }
19
+ //# sourceMappingURL=decode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.js","sourceRoot":"","sources":["../../src/audio/decode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAmB;IAC5D,MAAM,GAAG,GAAG,IAAI,YAAY,EAAE,CAAC;IAC/B,mGAAmG;IACnG,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IACpG,MAAM,OAAO,GAAG,IAAI,mBAAmB,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;IAC7F,MAAM,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAC5C,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC;IACxB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;IAChD,OAAO,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** Sample rate every STT/audio-LLM recipe in the SDK consumes (Whisper-style 16 kHz mono). */
2
+ export declare const AUDIO_SR = 16000;
3
+ /**
4
+ * Pitch-shift a mono PCM buffer by `ratio` (2 = up one octave, 0.5 = down one octave) while preserving
5
+ * duration. Time-domain granular overlap-add: Hann-windowed grains hop through the output at a fixed
6
+ * rate (so timing is unchanged) while each grain is read from the input at `ratio×` speed via linear
7
+ * interpolation (so pitch scales). A pure Float32Array→Float32Array function — no Web Audio — so pitch
8
+ * can be applied offline (baked into a clip) and unit-tested independently of a browser.
9
+ */
10
+ export declare function pitchShiftBuffer(input: Float32Array, ratio: number): Float32Array;
11
+ /** Encode a mono Float32 PCM buffer as a 16-bit PCM WAV Blob (used when pitch is baked into a clip). */
12
+ export declare function encodeWavBlob(samples: Float32Array, sampleRate: number): Blob;
13
+ //# sourceMappingURL=pcm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pcm.d.ts","sourceRoot":"","sources":["../../src/audio/pcm.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAsBjF;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CA0B7E"}
@@ -0,0 +1,66 @@
1
+ /** Sample rate every STT/audio-LLM recipe in the SDK consumes (Whisper-style 16 kHz mono). */
2
+ export const AUDIO_SR = 16000;
3
+ /**
4
+ * Pitch-shift a mono PCM buffer by `ratio` (2 = up one octave, 0.5 = down one octave) while preserving
5
+ * duration. Time-domain granular overlap-add: Hann-windowed grains hop through the output at a fixed
6
+ * rate (so timing is unchanged) while each grain is read from the input at `ratio×` speed via linear
7
+ * interpolation (so pitch scales). A pure Float32Array→Float32Array function — no Web Audio — so pitch
8
+ * can be applied offline (baked into a clip) and unit-tested independently of a browser.
9
+ */
10
+ export function pitchShiftBuffer(input, ratio) {
11
+ if (ratio === 1 || input.length === 0)
12
+ return input;
13
+ const grain = 1024;
14
+ const hop = grain / 4; // 75% overlap
15
+ const output = new Float32Array(input.length);
16
+ const weight = new Float32Array(input.length);
17
+ const hann = new Float32Array(grain);
18
+ for (let i = 0; i < grain; i += 1)
19
+ hann[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (grain - 1));
20
+ for (let out = 0; out + grain <= output.length; out += hop) {
21
+ for (let i = 0; i < grain; i += 1) {
22
+ const src = out + i * ratio; // grain anchored at `out` (time preserved), read at ratio× (pitch)
23
+ const i0 = Math.floor(src);
24
+ if (i0 + 1 >= input.length)
25
+ break;
26
+ const frac = src - i0;
27
+ const sample = input[i0] * (1 - frac) + input[i0 + 1] * frac;
28
+ output[out + i] += sample * hann[i];
29
+ weight[out + i] += hann[i];
30
+ }
31
+ }
32
+ for (let i = 0; i < output.length; i += 1)
33
+ if (weight[i] > 1e-6)
34
+ output[i] /= weight[i]; // OLA gain normalization
35
+ return output;
36
+ }
37
+ /** Encode a mono Float32 PCM buffer as a 16-bit PCM WAV Blob (used when pitch is baked into a clip). */
38
+ export function encodeWavBlob(samples, sampleRate) {
39
+ const buffer = new ArrayBuffer(44 + samples.length * 2);
40
+ const view = new DataView(buffer);
41
+ const writeStr = (offset, text) => {
42
+ for (let i = 0; i < text.length; i += 1)
43
+ view.setUint8(offset + i, text.charCodeAt(i));
44
+ };
45
+ writeStr(0, "RIFF");
46
+ view.setUint32(4, 36 + samples.length * 2, true);
47
+ writeStr(8, "WAVE");
48
+ writeStr(12, "fmt ");
49
+ view.setUint32(16, 16, true); // PCM fmt chunk size
50
+ view.setUint16(20, 1, true); // PCM format
51
+ view.setUint16(22, 1, true); // mono
52
+ view.setUint32(24, sampleRate, true);
53
+ view.setUint32(28, sampleRate * 2, true); // byte rate
54
+ view.setUint16(32, 2, true); // block align
55
+ view.setUint16(34, 16, true); // bits per sample
56
+ writeStr(36, "data");
57
+ view.setUint32(40, samples.length * 2, true);
58
+ let offset = 44;
59
+ for (let i = 0; i < samples.length; i += 1) {
60
+ const clamped = Math.max(-1, Math.min(1, samples[i]));
61
+ view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true);
62
+ offset += 2;
63
+ }
64
+ return new Blob([buffer], { type: "audio/wav" });
65
+ }
66
+ //# sourceMappingURL=pcm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pcm.js","sourceRoot":"","sources":["../../src/audio/pcm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Press-to-talk microphone recorder: getUserMedia → MediaRecorder with no timeslice (one blob at
3
+ * stop) → 16 kHz mono PCM. Hardened lifecycle ported from small-ai's pipeline recorder:
4
+ * - a synchronous pending claim so a double-press cannot start two recorders;
5
+ * - an inactive-recorder guard (the recorder auto-stops when its track ends — device unplugged,
6
+ * permission revoked — and `stop()` would then be a silent no-op whose `onstop` never fires);
7
+ * - try/finally teardown that always releases the mic tracks and closes the AudioContext.
8
+ *
9
+ * The recorder creates an AudioContext + MediaStreamSource so hosts can visualize input levels:
10
+ * `onStreamReady` receives them (attach an AnalyserNode there). The SDK never renders anything.
11
+ */
12
+ export type MicRecorderOptions = {
13
+ /** Called once per take after the mic is live — attach analyzers/visualizers here. */
14
+ onStreamReady?: (graph: {
15
+ context: AudioContext;
16
+ source: MediaStreamAudioSourceNode;
17
+ stream: MediaStream;
18
+ }) => void;
19
+ /** Called during teardown, before the context closes — dispose whatever onStreamReady created. */
20
+ onStreamEnded?: () => void;
21
+ };
22
+ export declare class MicRecorder {
23
+ #private;
24
+ constructor(options?: MicRecorderOptions);
25
+ /** Whether a take is currently being captured (true between a successful start() and stop()). */
26
+ get recording(): boolean;
27
+ /**
28
+ * Start capturing. No-op (returns false) when already recording or a start is mid-flight.
29
+ * @throws {MicrophoneError} when the mic cannot be acquired (`permissionDenied` set for denials).
30
+ */
31
+ start(): Promise<boolean>;
32
+ /**
33
+ * Stop the take and decode it to 16 kHz mono PCM. Returns null when nothing was recorded (no
34
+ * take in flight, or an empty capture). Always releases the mic, even if stopping throws.
35
+ */
36
+ stop(): Promise<Float32Array | null>;
37
+ /** Abandon a take without decoding it (releases the mic; the audio is discarded). Also aborts a start() still awaiting the permission prompt. */
38
+ cancel(): Promise<void>;
39
+ }
40
+ //# sourceMappingURL=recorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/audio/recorder.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,sFAAsF;IACtF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,YAAY,CAAC;QAAC,MAAM,EAAE,0BAA0B,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IACpH,kGAAkG;IAClG,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AASF,qBAAa,WAAW;;gBAMV,OAAO,GAAE,kBAAuB;IAI5C,iGAAiG;IACjG,IAAI,SAAS,IAAI,OAAO,CAEvB;IAUD;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;IAqC/B;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA2B1C,iJAAiJ;IAC3I,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;CAqB9B"}