@happyvertical/speech 0.101.3 → 0.102.1

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 (63) hide show
  1. package/README.md +36 -0
  2. package/dist/adapters/local/audio.d.ts +11 -1
  3. package/dist/adapters/local/env.d.ts +6 -2
  4. package/dist/adapters/local/runtime.d.ts +8 -1
  5. package/dist/adapters/local/transcriber.d.ts +3 -3
  6. package/dist/adapters/local/types.d.ts +7 -1
  7. package/dist/adapters/local/worker.d.ts +13 -3
  8. package/dist/adapters/openai-compatible-transcriber.d.ts +1 -1
  9. package/dist/adapters/openai-compatible.d.ts +1 -1
  10. package/dist/adapters/openai-realtime.d.ts +13 -2
  11. package/dist/adapters/qwen3.d.ts +1 -1
  12. package/dist/adapters/studio-server.d.ts +1 -1
  13. package/dist/adapters/voxtral-realtime.d.ts +36 -2
  14. package/dist/browser.d.ts +8 -0
  15. package/dist/browser.d.ts.map +1 -0
  16. package/dist/browser.js +159 -0
  17. package/dist/browser.js.map +1 -0
  18. package/dist/chunks/{errors-CQBX3OPS.js → errors-Cs0vX-mV.js} +25 -2
  19. package/dist/chunks/errors-Cs0vX-mV.js.map +1 -0
  20. package/dist/chunks/{registry-C_5BakmM.js → registry-DFSF3WoM.js} +2 -2
  21. package/dist/chunks/{registry-C_5BakmM.js.map → registry-DFSF3WoM.js.map} +1 -1
  22. package/dist/chunks/wav-B0Djpj2r.js +232 -0
  23. package/dist/chunks/wav-B0Djpj2r.js.map +1 -0
  24. package/dist/chunks/{websocket-CR9uXAuS.js → websocket-YbAfY6YI.js} +2 -2
  25. package/dist/chunks/{websocket-CR9uXAuS.js.map → websocket-YbAfY6YI.js.map} +1 -1
  26. package/dist/conversation/openai-webrtc.d.ts +1 -1
  27. package/dist/conversation-server.d.ts +3 -2
  28. package/dist/conversation-server.js +2 -2
  29. package/dist/conversation.js +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +35 -4
  33. package/dist/index.js.map +1 -1
  34. package/dist/local.d.ts +20 -2
  35. package/dist/local.js +2 -2
  36. package/dist/pcm.d.ts +8 -0
  37. package/dist/pcm.d.ts.map +1 -0
  38. package/dist/pcm.js +3 -0
  39. package/dist/shared/audio.d.ts +7 -1
  40. package/dist/shared/env.d.ts +8 -1
  41. package/dist/shared/errors.d.ts +18 -0
  42. package/dist/shared/errors.d.ts.map +1 -1
  43. package/dist/shared/factory.d.ts +2 -2
  44. package/dist/shared/http.d.ts +2 -2
  45. package/dist/shared/input-format.d.ts +20 -0
  46. package/dist/shared/input-format.d.ts.map +1 -0
  47. package/dist/shared/pcm-capture.d.ts +38 -0
  48. package/dist/shared/pcm-capture.d.ts.map +1 -0
  49. package/dist/shared/pcm.d.ts +6 -1
  50. package/dist/shared/realtime-auth.d.ts +10 -2
  51. package/dist/shared/realtime-session.d.ts +14 -5
  52. package/dist/shared/registry.d.ts +10 -2
  53. package/dist/shared/resample.d.ts +15 -0
  54. package/dist/shared/resample.d.ts.map +1 -0
  55. package/dist/shared/streaming-client-secret.d.ts +21 -3
  56. package/dist/shared/streaming-factory.d.ts +7 -3
  57. package/dist/shared/streaming-types.d.ts +10 -3
  58. package/dist/shared/turn-limit.d.ts +13 -1
  59. package/dist/shared/types.d.ts +4 -4
  60. package/dist/shared/wav.d.ts +52 -0
  61. package/dist/shared/wav.d.ts.map +1 -0
  62. package/package.json +9 -1
  63. package/dist/chunks/errors-CQBX3OPS.js.map +0 -1
package/README.md CHANGED
@@ -274,6 +274,42 @@ HAVE_SPEECH_STREAMING_MAX_TURN_SECONDS=270 # per-turn cap; Infinity remov
274
274
 
275
275
  A base URL may be `ws(s)://` or `http(s)://`: a server root gets `/v1/realtime`, a base ending in a version segment gets `/realtime`, and for `openai-realtime` `intent=transcription` is added unless an `intent` is present. `voxtral-realtime` requires a base URL. `createStreamingClientSecret()` reads the same `TYPE`, `BASE_URL` (mapped to `…/v1/realtime/client_secrets`), `API_KEY`, `MODEL`, `LANGUAGE`, `TURN_DETECTION`, `TIMEOUT`, and `HEADERS` variables. `HAVE_SPEECH_TRANSCRIBER_TYPE=openai-realtime` also routes `getTranscriber()` to the wrapped streaming adapter, which then reads `HAVE_SPEECH_STREAMING_*`.
276
276
 
277
+ ## Capturing audio for the realtime transcribers
278
+
279
+ `voxtral-realtime` and `openai-realtime` take raw 16-bit little-endian mono PCM (raw, or in a WAV) at one exact rate and reject compressed containers such as the webm/ogg a browser's `MediaRecorder` produces. `transcriberInputFormat(type)` says what to record:
280
+
281
+ | Type | Input |
282
+ | --- | --- |
283
+ | `voxtral-realtime` | `{ kind: 'pcm16', sampleRate: 16000, channels: 1 }` |
284
+ | `openai-realtime` | `{ kind: 'pcm16', sampleRate: 24000, channels: 1 }` (the default `audio/pcm` format; G.711 is an explicit adapter option) |
285
+ | `studio-server`, `openai-compatible`, `local` | `{ kind: 'compressed' }` (any container the backend or platform decodes) |
286
+
287
+ Three entry points, none of which loads Node-only code:
288
+
289
+ - `@happyvertical/speech` exports `transcriberInputFormat` and `TranscriberInputFormat`.
290
+ - `@happyvertical/speech/pcm` (browser, workers, Node): `encodeWavPcm16(samples, sampleRate)`, `parseWavPcm16(bytes, { sampleRate?, channels? })`, `resampleMono(samples, fromRate, toRate)`, `float32ToPcm16`, `pcm16ToFloat32`, and `WavFormatError` (stable `reason`).
291
+ - `@happyvertical/speech/browser` (SSR-safe to import): `createPcmCapture(stream, { sampleRate, maxDurationMs })`, `pcmCaptureSupported()`, and `PcmCaptureError`.
292
+
293
+ `parseWavPcm16` is strict because it reads untrusted bytes. It rejects, with `WavFormatError`, anything that is not a well-formed RIFF/WAVE with `fmt ` before `data`, format tag 1 (or `WAVE_FORMAT_EXTENSIBLE` whose subtype GUID is PCM), 16 bits, a consistent block align and byte rate, a non-empty data chunk that is a whole number of frames, and any declared size (RIFF, chunk or data) that exceeds the buffer. Nothing is clamped, nothing is allocated from a declared size, and work is linear in the input. It returns `{ sampleRate, channels, frames, durationMs, samples, data }`.
294
+
295
+ `resampleMono` is a windowed-sinc (Hann, 16 zero crossings per side) interpolator. Downsampling low-passes at 95% of the target Nyquist first, so a 12 kHz tone does not alias when going 48 kHz to 16 kHz. It is built for speech recognition, not mastering, and output length is `floor(n * to / from)`, capped at 2^28 samples.
296
+
297
+ `createPcmCapture` uses `AudioContext` + `AudioWorklet` (worklet inlined via a Blob URL, no bundler setup), mixes all channels to mono, drops audio past `maxDurationMs` (`truncated: true`, optional `onLimit`), and on `stop()` or `cancel()` disconnects the nodes, closes the context and revokes the Blob URL. You keep ownership of the `MediaStream` and its tracks.
298
+
299
+ ```typescript
300
+ import { createPcmCapture } from '@happyvertical/speech/browser';
301
+ import { transcriberInputFormat } from '@happyvertical/speech';
302
+
303
+ const format = transcriberInputFormat('voxtral-realtime');
304
+ if (format.kind !== 'pcm16') throw new Error('expected a raw PCM transcriber');
305
+ const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
306
+ const capture = createPcmCapture(mic, { sampleRate: format.sampleRate, maxDurationMs: 30_000 });
307
+ // ...on release:
308
+ const { wav } = await capture.stop(); // mono 16-bit WAV at format.sampleRate
309
+ for (const track of mic.getTracks()) track.stop();
310
+ await fetch('/transcribe', { method: 'POST', headers: { 'Content-Type': 'audio/wav' }, body: wav });
311
+ ```
312
+
277
313
  ## On-device Transcription
278
314
 
279
315
  `type: 'local'` runs Whisper or Moonshine ONNX models on the device with [transformers.js](https://huggingface.co/docs/transformers.js). In a browser it uses WebGPU or WASM; in Node it uses onnxruntime-node. Audio never leaves the device, there is no API key or per-call cost, and once the model is cached it works offline.
@@ -1,4 +1,14 @@
1
- import { DecodedAudio, LocalAudioDecoder } from './types.js';
1
+ /**
2
+ * Audio decoding for on-device inference: turns encoded bytes into 16 kHz
3
+ * mono Float32 PCM, the input Whisper and Moonshine expect.
4
+ *
5
+ * - WAV (PCM 8/16/24/32-bit, IEEE float 32/64, WAVE_FORMAT_EXTENSIBLE) and raw
6
+ * `audio/pcm` are decoded in-process in every runtime, including workers.
7
+ * - Anything else goes to the caller's `decodeAudio` hook, or, in browsers,
8
+ * to `OfflineAudioContext.decodeAudioData` (MediaRecorder WebM/Opus, Safari
9
+ * `audio/mp4`, MP3, ...).
10
+ */
11
+ import type { DecodedAudio, LocalAudioDecoder } from './types.js';
2
12
  /** Sample rate expected by Whisper and Moonshine feature extractors. */
3
13
  export declare const LOCAL_SAMPLE_RATE = 16000;
4
14
  export interface DecodeToPcmOptions {
@@ -1,5 +1,9 @@
1
- import { SpeechFactoryContext } from '../../shared/factory.js';
2
- import { LocalTranscriberOptions } from './types.js';
1
+ /**
2
+ * Environment configuration for the `local` transcriber. Explicit options win,
3
+ * then the factory context, then `HAVE_SPEECH_TRANSCRIBER_*` variables (Node).
4
+ */
5
+ import type { SpeechFactoryContext } from '../../shared/factory.js';
6
+ import type { LocalTranscriberOptions } from './types.js';
3
7
  /** Local-only environment variables, in priority order. */
4
8
  export declare const LOCAL_TRANSCRIBER_ENV_KEYS: {
5
9
  readonly device: readonly ["HAVE_SPEECH_TRANSCRIBER_DEVICE"];
@@ -1,4 +1,11 @@
1
- import { LocalTranscriberDevice, LocalTranscriberDtype, LocalTranscriberProgressCallback, LocalTransformersRuntime } from './types.js';
1
+ /**
2
+ * Loads the optional `@huggingface/transformers` peer dependency and builds
3
+ * `automatic-speech-recognition` pipelines from it.
4
+ *
5
+ * This is the only module that names the peer. It is reachable only from the
6
+ * `@happyvertical/speech/local` entry, never from the core entry.
7
+ */
8
+ import type { LocalTranscriberDevice, LocalTranscriberDtype, LocalTranscriberProgressCallback, LocalTransformersRuntime } from './types.js';
2
9
  export declare const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
3
10
  export declare const TRANSFORMERS_INSTALL_HINT: string;
4
11
  /** One ASR result as returned by transformers.js. */
@@ -1,6 +1,6 @@
1
- import { Transcriber, TranscriptionRequest, TranscriptResult } from '../../shared/types.js';
2
- import { AsrOutput } from './runtime.js';
3
- import { LocalTranscriberOptions } from './types.js';
1
+ import type { Transcriber, TranscriptionRequest, TranscriptResult } from '../../shared/types.js';
2
+ import { type AsrOutput } from './runtime.js';
3
+ import type { LocalTranscriberOptions } from './types.js';
4
4
  /** Small multilingual Whisper (~80 MB at the default q8 quantisation). */
5
5
  export declare const DEFAULT_LOCAL_MODEL = "onnx-community/whisper-base";
6
6
  /**
@@ -1,4 +1,10 @@
1
- import { SpeechUsageCallback } from '../../shared/usage.js';
1
+ /**
2
+ * Public option types for the on-device (`local`) transcriber.
3
+ *
4
+ * Kept free of `@huggingface/transformers` types so the core entry can
5
+ * reference them without depending on the optional peer.
6
+ */
7
+ import type { SpeechUsageCallback } from '../../shared/usage.js';
2
8
  /**
3
9
  * Inference device. `auto` picks WebGPU when the browser exposes a usable
4
10
  * adapter (falling back to WASM if WebGPU fails to initialise) and `cpu` in
@@ -1,6 +1,16 @@
1
- import { TimestampGranularity, Transcriber, TranscriptionRequest, TranscriptResult } from '../../shared/types.js';
2
- import { SpeechUsageCallback } from '../../shared/usage.js';
3
- import { LocalAudioDecoder, LocalTranscriberOptions, LocalTranscriberProgress, LocalTranscriberProgressCallback } from './types.js';
1
+ /**
2
+ * Web Worker support for the `local` transcriber.
3
+ *
4
+ * Inference runs in a worker (`serveLocalTranscriber`) so long recordings never
5
+ * block the UI thread. The main-thread client (`LocalTranscriberWorkerClient`)
6
+ * transfers WAV and raw PCM bytes to the worker, which parses, downmixes, and
7
+ * resamples them there. Only formats that need `AudioContext` (which workers
8
+ * lack) or the client's `decodeAudio` hook are decoded on the main thread;
9
+ * their Float32 channels are then transferred and resampled in the worker.
10
+ */
11
+ import type { TimestampGranularity, Transcriber, TranscriptionRequest, TranscriptResult } from '../../shared/types.js';
12
+ import { type SpeechUsageCallback } from '../../shared/usage.js';
13
+ import type { LocalAudioDecoder, LocalTranscriberOptions, LocalTranscriberProgress, LocalTranscriberProgressCallback } from './types.js';
4
14
  /** Minimal `postMessage` endpoint: a `Worker`, `DedicatedWorkerGlobalScope`, or `MessagePort`. */
5
15
  export interface LocalWorkerEndpoint {
6
16
  postMessage(message: unknown, transfer?: Transferable[]): void;
@@ -1,5 +1,5 @@
1
1
  import { HttpSpeechAdapter } from '../shared/http.js';
2
- import { OpenAICompatibleTranscriberOptions, Transcriber, TranscriptionRequest, TranscriptResult } from '../shared/types.js';
2
+ import type { OpenAICompatibleTranscriberOptions, Transcriber, TranscriptionRequest, TranscriptResult } from '../shared/types.js';
3
3
  export declare function isJsonOnlyTranscriptionModel(model: string): boolean;
4
4
  /**
5
5
  * OpenAI-compatible speech-to-text adapter (`POST <base>/audio/transcriptions`).
@@ -1,5 +1,5 @@
1
1
  import { HttpSpeechAdapter } from '../shared/http.js';
2
- import { OpenAICompatibleSpeechSynthesizerOptions, SpeechSynthesizer, SynthesisRequest, SynthesizedSpeech } from '../shared/types.js';
2
+ import type { OpenAICompatibleSpeechSynthesizerOptions, SpeechSynthesizer, SynthesisRequest, SynthesizedSpeech } from '../shared/types.js';
3
3
  export declare class OpenAICompatibleSpeechSynthesizer extends HttpSpeechAdapter implements SpeechSynthesizer {
4
4
  readonly type: "openai-compatible";
5
5
  /** Explicit override, resolved relative to `baseUrl` (or absolute). */
@@ -1,5 +1,16 @@
1
- import { RealtimeProtocol, RealtimeProtocolEvent, RealtimeSessionConfig } from '../shared/realtime-session.js';
2
- import { OpenAIRealtimeTranscriberOptions, StreamingAudioFormat, StreamingSession, StreamingSessionOptions, StreamingTranscriber, StreamingTurnLimit } from '../shared/streaming-types.js';
1
+ /**
2
+ * OpenAI Realtime transcription adapter (`openai-realtime`).
3
+ *
4
+ * Targets the GA Realtime API transcription session (no `OpenAI-Beta`
5
+ * header): connect to `wss://api.openai.com/v1/realtime?intent=transcription`,
6
+ * send `session.update` with `session.type: "transcription"`, stream
7
+ * `input_audio_buffer.append`, and read
8
+ * `conversation.item.input_audio_transcription.delta` / `.completed`.
9
+ * Reference: https://developers.openai.com/api/docs/guides/realtime-transcription
10
+ * and https://developers.openai.com/api/reference/resources/realtime/client-events
11
+ */
12
+ import { type RealtimeProtocol, type RealtimeProtocolEvent, type RealtimeSessionConfig } from '../shared/realtime-session.js';
13
+ import type { OpenAIRealtimeTranscriberOptions, StreamingAudioFormat, StreamingSession, StreamingSessionOptions, StreamingTranscriber, StreamingTurnLimit } from '../shared/streaming-types.js';
3
14
  export declare const OPENAI_REALTIME_DEFAULT_URL = "wss://api.openai.com/v1/realtime";
4
15
  export declare const OPENAI_REALTIME_DEFAULT_MODEL = "gpt-4o-transcribe";
5
16
  /** The OpenAI Realtime transcription wire protocol. */
@@ -1,5 +1,5 @@
1
1
  import { HttpSpeechAdapter } from '../shared/http.js';
2
- import { Qwen3SpeechSynthesizerOptions, SpeechSynthesizer, SynthesisRequest, SynthesizedSpeech } from '../shared/types.js';
2
+ import type { Qwen3SpeechSynthesizerOptions, SpeechSynthesizer, SynthesisRequest, SynthesizedSpeech } from '../shared/types.js';
3
3
  export declare class Qwen3SpeechSynthesizer extends HttpSpeechAdapter implements SpeechSynthesizer {
4
4
  readonly type: "qwen3-tts";
5
5
  private readonly speechPath;
@@ -1,5 +1,5 @@
1
1
  import { HttpSpeechAdapter } from '../shared/http.js';
2
- import { SpeechSynthesizer, StudioServerSpeechSynthesizerOptions, StudioServerTranscriberOptions, SynthesisRequest, SynthesizedSpeech, Transcriber, TranscriptionRequest, TranscriptResult } from '../shared/types.js';
2
+ import type { SpeechSynthesizer, StudioServerSpeechSynthesizerOptions, StudioServerTranscriberOptions, SynthesisRequest, SynthesizedSpeech, Transcriber, TranscriptionRequest, TranscriptResult } from '../shared/types.js';
3
3
  export declare class StudioServerTranscriber extends HttpSpeechAdapter implements Transcriber {
4
4
  readonly type: "studio-server";
5
5
  private readonly transcribePath;
@@ -1,5 +1,39 @@
1
- import { RealtimeProtocol, RealtimeProtocolEvent } from '../shared/realtime-session.js';
2
- import { StreamingAudioFormat, StreamingSession, StreamingSessionOptions, StreamingTranscriber, StreamingTurnLimit, VoxtralRealtimeTranscriberOptions } from '../shared/streaming-types.js';
1
+ /**
2
+ * vLLM realtime transcription adapter (`voxtral-realtime`), for Mistral's
3
+ * Voxtral Realtime models (e.g. `mistralai/Voxtral-Mini-4B-Realtime-2602`)
4
+ * served by vLLM at `/v1/realtime`.
5
+ *
6
+ * vLLM's protocol borrows OpenAI event names but is not OpenAI-compatible.
7
+ * Verified against vLLM's `entrypoints/speech_to_text/realtime` source and a
8
+ * live server:
9
+ *
10
+ * 1. The server sends `session.created` (`{ id, created }`) on connect.
11
+ * 2. The client sends `session.update` with a top-level `model` (no nested
12
+ * `session` object, no acknowledgement; an unknown model yields an
13
+ * `error` with code `model_not_found`).
14
+ * 3. A non-final `input_audio_buffer.commit` starts generation for a turn.
15
+ * 4. `input_audio_buffer.append` carries base64 PCM16 at 16 kHz mono. Audio
16
+ * appended before the start commit is kept.
17
+ * 5. The server streams `transcription.delta` (`{ delta }`, often empty) while
18
+ * audio arrives, at roughly real-time speed.
19
+ * 6. `input_audio_buffer.commit` with `final: true` ends the turn; the server
20
+ * replies `transcription.done` (`{ text, usage }`, token usage only) and
21
+ * clears its buffer. A further start commit opens the next turn on the same
22
+ * socket.
23
+ * 7. Errors arrive as `{ type: 'error', error: string, code }` and leave the
24
+ * socket open; this adapter treats every error as fatal.
25
+ * 8. A turn whose audio and tokens exceed `max_model_len` (~12.5 tokens per
26
+ * audio second plus a 39-token prompt) ends in `error` `processing_error`
27
+ * and can take the engine down. The limit is per turn: after a final
28
+ * commit vLLM clears the turn's context. The session therefore rolls over
29
+ * to a new turn before `maxTurnSeconds` (default 270) of audio.
30
+ * 9. A stray final commit leaves a stale end marker: the next turn ends at once
31
+ * with `prompt_tokens: 1` (any audio costs 39) and its audio is dropped.
32
+ * The session fails closed on that, and on any `done` for a turn it did
33
+ * not commit, rather than losing audio silently.
34
+ */
35
+ import { type RealtimeProtocol, type RealtimeProtocolEvent } from '../shared/realtime-session.js';
36
+ import type { StreamingAudioFormat, StreamingSession, StreamingSessionOptions, StreamingTranscriber, StreamingTurnLimit, VoxtralRealtimeTranscriberOptions } from '../shared/streaming-types.js';
3
37
  /** vLLM's default served model name for Voxtral Mini Realtime. */
4
38
  export declare const VOXTRAL_REALTIME_DEFAULT_MODEL = "mistralai/Voxtral-Mini-4B-Realtime-2602";
5
39
  /** The only input format vLLM's realtime endpoint accepts. */
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `@happyvertical/speech/browser`: capture microphone audio as the raw PCM the
3
+ * realtime transcribers need. No Node APIs; safe to import during SSR (it only
4
+ * touches Web Audio when `createPcmCapture` is called).
5
+ */
6
+ export { PcmCaptureError, type PcmCaptureErrorReason, } from './shared/errors.js';
7
+ export { createPcmCapture, type PcmCapture, type PcmCaptureOptions, type PcmCaptureResult, pcmCaptureSupported, } from './shared/pcm-capture.js';
8
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,mBAAmB,GACpB,MAAM,yBAAyB,CAAC"}
@@ -0,0 +1,159 @@
1
+ import { n as PcmCaptureError } from "./chunks/errors-Cs0vX-mV.js";
2
+ import { o as resampleMono, t as encodeWavPcm16 } from "./chunks/wav-B0Djpj2r.js";
3
+ //#region src/shared/pcm-capture.ts
4
+ /**
5
+ * Browser PCM capture: `MediaStream` -> mono Float32 at a requested rate and a
6
+ * ready 16-bit PCM WAV, via `AudioContext` + `AudioWorklet`. The worklet code
7
+ * is inlined and loaded from a Blob URL, so no bundler configuration is
8
+ * needed. Nothing here touches Node APIs; the Web Audio globals are read when
9
+ * `createPcmCapture` is called, so importing this module is SSR-safe.
10
+ */
11
+ var PROCESSOR_NAME = "hv-pcm-tap";
12
+ var WORKLET_SOURCE = `
13
+ class HvPcmTap extends AudioWorkletProcessor {
14
+ process(inputs) {
15
+ const channels = inputs[0];
16
+ if (channels && channels.length > 0 && channels[0].length > 0) {
17
+ const frames = channels[0].length;
18
+ const mono = new Float32Array(frames);
19
+ for (let c = 0; c < channels.length; c += 1) {
20
+ const data = channels[c];
21
+ for (let i = 0; i < frames; i += 1) mono[i] += data[i];
22
+ }
23
+ if (channels.length > 1) {
24
+ for (let i = 0; i < frames; i += 1) mono[i] /= channels.length;
25
+ }
26
+ this.port.postMessage(mono, [mono.buffer]);
27
+ }
28
+ return true;
29
+ }
30
+ }
31
+ registerProcessor('${PROCESSOR_NAME}', HvPcmTap);
32
+ `;
33
+ /** Whether this runtime has the Web Audio pieces `createPcmCapture` needs. */
34
+ function pcmCaptureSupported() {
35
+ return typeof AudioContext !== "undefined" && typeof AudioWorkletNode !== "undefined" && typeof Blob !== "undefined" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function";
36
+ }
37
+ /**
38
+ * Starts capturing `stream`. Throws `PcmCaptureError` (`unsupported`) when Web
39
+ * Audio is unavailable. The caller keeps ownership of the stream and its
40
+ * tracks: stop them yourself after `stop()` or `cancel()`.
41
+ */
42
+ function createPcmCapture(stream, options) {
43
+ const { sampleRate, maxDurationMs } = options;
44
+ if (!Number.isInteger(sampleRate) || sampleRate < 1) throw new RangeError("sampleRate must be a positive integer");
45
+ if (!Number.isFinite(maxDurationMs) || maxDurationMs <= 0) throw new RangeError("maxDurationMs must be a positive number");
46
+ if (!pcmCaptureSupported()) throw new PcmCaptureError("unsupported", "PCM capture needs AudioContext, AudioWorkletNode and Blob URLs");
47
+ const context = new AudioContext();
48
+ const capSamples = Math.ceil(maxDurationMs / 1e3 * context.sampleRate);
49
+ const frames = [];
50
+ let captured = 0;
51
+ let truncated = false;
52
+ let limitFired = false;
53
+ let cancelled = false;
54
+ let finished = false;
55
+ let url;
56
+ let source;
57
+ let node;
58
+ let closing;
59
+ const onFrame = (event) => {
60
+ if (finished) return;
61
+ let frame = event.data;
62
+ const room = capSamples - captured;
63
+ if (frame.length > room) {
64
+ frame = frame.subarray(0, Math.max(0, room));
65
+ truncated = true;
66
+ }
67
+ if (frame.length > 0) {
68
+ frames.push(frame);
69
+ captured += frame.length;
70
+ }
71
+ if (captured >= capSamples && !limitFired) {
72
+ limitFired = true;
73
+ options.onLimit?.();
74
+ }
75
+ };
76
+ const revoke = () => {
77
+ if (url !== void 0) {
78
+ const stale = url;
79
+ url = void 0;
80
+ try {
81
+ URL.revokeObjectURL(stale);
82
+ } catch {}
83
+ }
84
+ };
85
+ const teardown = () => {
86
+ if (closing) return closing;
87
+ finished = true;
88
+ revoke();
89
+ if (node) node.port.onmessage = null;
90
+ try {
91
+ source?.disconnect();
92
+ node?.disconnect();
93
+ } catch {}
94
+ closing = (async () => {
95
+ try {
96
+ if (context.state !== "closed") await context.close();
97
+ } catch {}
98
+ })();
99
+ return closing;
100
+ };
101
+ const ready = (async () => {
102
+ url = URL.createObjectURL(new Blob([WORKLET_SOURCE], { type: "text/javascript" }));
103
+ try {
104
+ await context.audioWorklet.addModule(url);
105
+ } finally {
106
+ revoke();
107
+ }
108
+ if (finished) return;
109
+ source = context.createMediaStreamSource(stream);
110
+ node = new AudioWorkletNode(context, PROCESSOR_NAME, {
111
+ numberOfInputs: 1,
112
+ numberOfOutputs: 1,
113
+ outputChannelCount: [1]
114
+ });
115
+ node.port.onmessage = onFrame;
116
+ source.connect(node);
117
+ node.connect(context.destination);
118
+ if (context.state === "suspended") await context.resume();
119
+ })();
120
+ ready.catch(() => void 0);
121
+ let result;
122
+ return {
123
+ stop() {
124
+ result ??= (async () => {
125
+ try {
126
+ await ready;
127
+ } catch (error) {
128
+ await teardown();
129
+ throw new PcmCaptureError("setup_failed", "PCM capture could not start", error);
130
+ }
131
+ await teardown();
132
+ if (cancelled) throw new PcmCaptureError("cancelled", "PCM capture was cancelled");
133
+ const joined = new Float32Array(captured);
134
+ let offset = 0;
135
+ for (const frame of frames.splice(0)) {
136
+ joined.set(frame, offset);
137
+ offset += frame.length;
138
+ }
139
+ const samples = resampleMono(joined, context.sampleRate, sampleRate);
140
+ return {
141
+ samples,
142
+ wav: encodeWavPcm16(samples, sampleRate),
143
+ truncated
144
+ };
145
+ })();
146
+ return result;
147
+ },
148
+ cancel() {
149
+ if (cancelled) return;
150
+ cancelled = true;
151
+ frames.length = 0;
152
+ teardown();
153
+ }
154
+ };
155
+ }
156
+ //#endregion
157
+ export { PcmCaptureError, createPcmCapture, pcmCaptureSupported };
158
+
159
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","names":[],"sources":["../src/shared/pcm-capture.ts"],"sourcesContent":["/**\n * Browser PCM capture: `MediaStream` -> mono Float32 at a requested rate and a\n * ready 16-bit PCM WAV, via `AudioContext` + `AudioWorklet`. The worklet code\n * is inlined and loaded from a Blob URL, so no bundler configuration is\n * needed. Nothing here touches Node APIs; the Web Audio globals are read when\n * `createPcmCapture` is called, so importing this module is SSR-safe.\n */\n\nimport { PcmCaptureError } from './errors.js';\nimport { resampleMono } from './resample.js';\nimport { encodeWavPcm16 } from './wav.js';\n\nconst PROCESSOR_NAME = 'hv-pcm-tap';\n\n// Mixes every input channel to mono and posts one transferable block per\n// render quantum. Returns true to stay alive until the node is disconnected.\nconst WORKLET_SOURCE = `\nclass HvPcmTap extends AudioWorkletProcessor {\n process(inputs) {\n const channels = inputs[0];\n if (channels && channels.length > 0 && channels[0].length > 0) {\n const frames = channels[0].length;\n const mono = new Float32Array(frames);\n for (let c = 0; c < channels.length; c += 1) {\n const data = channels[c];\n for (let i = 0; i < frames; i += 1) mono[i] += data[i];\n }\n if (channels.length > 1) {\n for (let i = 0; i < frames; i += 1) mono[i] /= channels.length;\n }\n this.port.postMessage(mono, [mono.buffer]);\n }\n return true;\n }\n}\nregisterProcessor('${PROCESSOR_NAME}', HvPcmTap);\n`;\n\nexport interface PcmCaptureOptions {\n /** Rate of the returned samples and WAV, in Hz (e.g. 16000 for Voxtral). */\n sampleRate: number;\n /** Audio beyond this duration is dropped, not stored. */\n maxDurationMs: number;\n /** Called once, when the cap is reached; the capture keeps running until stopped. */\n onLimit?: () => void;\n}\n\nexport interface PcmCaptureResult {\n /** Mono samples at `sampleRate`, -1..1. */\n samples: Float32Array;\n /** The same audio as a 16-bit PCM mono WAV. */\n wav: Uint8Array;\n /** True when audio past `maxDurationMs` was dropped. */\n truncated: boolean;\n}\n\nexport interface PcmCapture {\n /** Ends capture, tears everything down, and resolves with the audio. Idempotent. */\n stop(): Promise<PcmCaptureResult>;\n /** Discards the audio and tears everything down. Idempotent; a pending `stop()` rejects. */\n cancel(): void;\n}\n\n/** Whether this runtime has the Web Audio pieces `createPcmCapture` needs. */\nexport function pcmCaptureSupported(): boolean {\n return (\n typeof AudioContext !== 'undefined' &&\n typeof AudioWorkletNode !== 'undefined' &&\n typeof Blob !== 'undefined' &&\n typeof URL !== 'undefined' &&\n typeof URL.createObjectURL === 'function'\n );\n}\n\n/**\n * Starts capturing `stream`. Throws `PcmCaptureError` (`unsupported`) when Web\n * Audio is unavailable. The caller keeps ownership of the stream and its\n * tracks: stop them yourself after `stop()` or `cancel()`.\n */\nexport function createPcmCapture(\n stream: MediaStream,\n options: PcmCaptureOptions,\n): PcmCapture {\n const { sampleRate, maxDurationMs } = options;\n if (!Number.isInteger(sampleRate) || sampleRate < 1) {\n throw new RangeError('sampleRate must be a positive integer');\n }\n if (!Number.isFinite(maxDurationMs) || maxDurationMs <= 0) {\n throw new RangeError('maxDurationMs must be a positive number');\n }\n if (!pcmCaptureSupported()) {\n throw new PcmCaptureError(\n 'unsupported',\n 'PCM capture needs AudioContext, AudioWorkletNode and Blob URLs',\n );\n }\n\n const context = new AudioContext();\n const capSamples = Math.ceil((maxDurationMs / 1000) * context.sampleRate);\n const frames: Float32Array[] = [];\n let captured = 0;\n let truncated = false;\n let limitFired = false;\n let cancelled = false;\n let finished = false;\n let url: string | undefined;\n let source: MediaStreamAudioSourceNode | undefined;\n let node: AudioWorkletNode | undefined;\n let closing: Promise<void> | undefined;\n\n const onFrame = (event: MessageEvent<Float32Array>): void => {\n if (finished) return;\n let frame = event.data;\n const room = capSamples - captured;\n if (frame.length > room) {\n frame = frame.subarray(0, Math.max(0, room));\n truncated = true;\n }\n if (frame.length > 0) {\n frames.push(frame);\n captured += frame.length;\n }\n if (captured >= capSamples && !limitFired) {\n limitFired = true;\n options.onLimit?.();\n }\n };\n\n const revoke = (): void => {\n if (url !== undefined) {\n const stale = url;\n url = undefined;\n try {\n URL.revokeObjectURL(stale);\n } catch {\n // already revoked\n }\n }\n };\n\n const teardown = (): Promise<void> => {\n if (closing) return closing;\n finished = true;\n revoke();\n if (node) node.port.onmessage = null;\n try {\n source?.disconnect();\n node?.disconnect();\n } catch {\n // graph already torn down\n }\n closing = (async () => {\n try {\n if (context.state !== 'closed') await context.close();\n } catch {\n // closing a dead context is not worth reporting\n }\n })();\n return closing;\n };\n\n const ready = (async (): Promise<void> => {\n url = URL.createObjectURL(\n new Blob([WORKLET_SOURCE], { type: 'text/javascript' }),\n );\n try {\n await context.audioWorklet.addModule(url);\n } finally {\n revoke();\n }\n if (finished) return;\n source = context.createMediaStreamSource(stream);\n node = new AudioWorkletNode(context, PROCESSOR_NAME, {\n numberOfInputs: 1,\n numberOfOutputs: 1,\n outputChannelCount: [1],\n });\n node.port.onmessage = onFrame;\n source.connect(node);\n // A worklet only runs while connected to the destination; it emits silence.\n node.connect(context.destination);\n if (context.state === 'suspended') await context.resume();\n })();\n // Failure is reported by stop(); never an unhandled rejection.\n ready.catch(() => undefined);\n\n let result: Promise<PcmCaptureResult> | undefined;\n\n return {\n stop(): Promise<PcmCaptureResult> {\n result ??= (async () => {\n try {\n await ready;\n } catch (error) {\n await teardown();\n throw new PcmCaptureError(\n 'setup_failed',\n 'PCM capture could not start',\n error,\n );\n }\n await teardown();\n if (cancelled) {\n throw new PcmCaptureError('cancelled', 'PCM capture was cancelled');\n }\n const joined = new Float32Array(captured);\n let offset = 0;\n for (const frame of frames.splice(0)) {\n joined.set(frame, offset);\n offset += frame.length;\n }\n const samples = resampleMono(joined, context.sampleRate, sampleRate);\n return { samples, wav: encodeWavPcm16(samples, sampleRate), truncated };\n })();\n return result;\n },\n cancel(): void {\n if (cancelled) return;\n cancelled = true;\n frames.length = 0;\n void teardown();\n },\n };\n}\n"],"mappings":";;;;;;;;;;AAYA,IAAM,iBAAiB;AAIvB,IAAM,iBAAiB;;;;;;;;;;;;;;;;;;;qBAmBF,eAAe;;;AA6BpC,SAAgB,sBAA+B;CAC7C,OACE,OAAO,iBAAiB,eACxB,OAAO,qBAAqB,eAC5B,OAAO,SAAS,eAChB,OAAO,QAAQ,eACf,OAAO,IAAI,oBAAoB;AAEnC;;;;;;AAOA,SAAgB,iBACd,QACA,SACY;CACZ,MAAM,EAAE,YAAY,kBAAkB;CACtC,IAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,GAChD,MAAM,IAAI,WAAW,uCAAuC;CAE9D,IAAI,CAAC,OAAO,SAAS,aAAa,KAAK,iBAAiB,GACtD,MAAM,IAAI,WAAW,yCAAyC;CAEhE,IAAI,CAAC,oBAAoB,GACvB,MAAM,IAAI,gBACR,eACA,gEACF;CAGF,MAAM,UAAU,IAAI,aAAa;CACjC,MAAM,aAAa,KAAK,KAAM,gBAAgB,MAAQ,QAAQ,UAAU;CACxE,MAAM,SAAyB,CAAC;CAChC,IAAI,WAAW;CACf,IAAI,YAAY;CAChB,IAAI,aAAa;CACjB,IAAI,YAAY;CAChB,IAAI,WAAW;CACf,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,WAAW,UAA4C;EAC3D,IAAI,UAAU;EACd,IAAI,QAAQ,MAAM;EAClB,MAAM,OAAO,aAAa;EAC1B,IAAI,MAAM,SAAS,MAAM;GACvB,QAAQ,MAAM,SAAS,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;GAC3C,YAAY;EACd;EACA,IAAI,MAAM,SAAS,GAAG;GACpB,OAAO,KAAK,KAAK;GACjB,YAAY,MAAM;EACpB;EACA,IAAI,YAAY,cAAc,CAAC,YAAY;GACzC,aAAa;GACb,QAAQ,UAAU;EACpB;CACF;CAEA,MAAM,eAAqB;EACzB,IAAI,QAAQ,KAAA,GAAW;GACrB,MAAM,QAAQ;GACd,MAAM,KAAA;GACN,IAAI;IACF,IAAI,gBAAgB,KAAK;GAC3B,QAAQ,CAER;EACF;CACF;CAEA,MAAM,iBAAgC;EACpC,IAAI,SAAS,OAAO;EACpB,WAAW;EACX,OAAO;EACP,IAAI,MAAM,KAAK,KAAK,YAAY;EAChC,IAAI;GACF,QAAQ,WAAW;GACnB,MAAM,WAAW;EACnB,QAAQ,CAER;EACA,WAAW,YAAY;GACrB,IAAI;IACF,IAAI,QAAQ,UAAU,UAAU,MAAM,QAAQ,MAAM;GACtD,QAAQ,CAER;EACF,EAAA,CAAG;EACH,OAAO;CACT;CAEA,MAAM,SAAS,YAA2B;EACxC,MAAM,IAAI,gBACR,IAAI,KAAK,CAAC,cAAc,GAAG,EAAE,MAAM,kBAAkB,CAAC,CACxD;EACA,IAAI;GACF,MAAM,QAAQ,aAAa,UAAU,GAAG;EAC1C,UAAU;GACR,OAAO;EACT;EACA,IAAI,UAAU;EACd,SAAS,QAAQ,wBAAwB,MAAM;EAC/C,OAAO,IAAI,iBAAiB,SAAS,gBAAgB;GACnD,gBAAgB;GAChB,iBAAiB;GACjB,oBAAoB,CAAC,CAAC;EACxB,CAAC;EACD,KAAK,KAAK,YAAY;EACtB,OAAO,QAAQ,IAAI;EAEnB,KAAK,QAAQ,QAAQ,WAAW;EAChC,IAAI,QAAQ,UAAU,aAAa,MAAM,QAAQ,OAAO;CAC1D,EAAA,CAAG;CAEH,MAAM,YAAY,KAAA,CAAS;CAE3B,IAAI;CAEJ,OAAO;EACL,OAAkC;GAChC,YAAY,YAAY;IACtB,IAAI;KACF,MAAM;IACR,SAAS,OAAO;KACd,MAAM,SAAS;KACf,MAAM,IAAI,gBACR,gBACA,+BACA,KACF;IACF;IACA,MAAM,SAAS;IACf,IAAI,WACF,MAAM,IAAI,gBAAgB,aAAa,2BAA2B;IAEpE,MAAM,SAAS,IAAI,aAAa,QAAQ;IACxC,IAAI,SAAS;IACb,KAAK,MAAM,SAAS,OAAO,OAAO,CAAC,GAAG;KACpC,OAAO,IAAI,OAAO,MAAM;KACxB,UAAU,MAAM;IAClB;IACA,MAAM,UAAU,aAAa,QAAQ,QAAQ,YAAY,UAAU;IACnE,OAAO;KAAE;KAAS,KAAK,eAAe,SAAS,UAAU;KAAG;IAAU;GACxE,EAAA,CAAG;GACH,OAAO;EACT;EACA,SAAe;GACb,IAAI,WAAW;GACf,YAAY;GACZ,OAAO,SAAS;GAChB,SAAc;EAChB;CACF;AACF"}
@@ -35,7 +35,30 @@ var SpeechProviderError = class extends SpeechError {
35
35
  if (options.cause !== void 0) this.cause = options.cause;
36
36
  }
37
37
  };
38
+ /**
39
+ * Thrown by the PCM/WAV utilities (`@happyvertical/speech/pcm`) for input that
40
+ * is not exactly the 16-bit PCM WAV that was asked for, and for invalid
41
+ * arguments (sample rate, sizes). `reason` is stable and machine-readable.
42
+ */
43
+ var WavFormatError = class extends SpeechError {
44
+ reason;
45
+ constructor(reason, message) {
46
+ super(message, "SPEECH_INVALID_WAV");
47
+ this.name = "WavFormatError";
48
+ this.reason = reason;
49
+ }
50
+ };
51
+ /** Thrown by `createPcmCapture` (`@happyvertical/speech/browser`). */
52
+ var PcmCaptureError = class extends SpeechError {
53
+ reason;
54
+ constructor(reason, message, cause) {
55
+ super(message, "SPEECH_PCM_CAPTURE");
56
+ this.name = "PcmCaptureError";
57
+ this.reason = reason;
58
+ if (cause !== void 0) this.cause = cause;
59
+ }
60
+ };
38
61
  //#endregion
39
- export { SpeechProviderError as i, SpeechConfigurationError as n, SpeechError as r, InvalidSpeechAdapterError as t };
62
+ export { SpeechProviderError as a, SpeechError as i, PcmCaptureError as n, WavFormatError as o, SpeechConfigurationError as r, InvalidSpeechAdapterError as t };
40
63
 
41
- //# sourceMappingURL=errors-CQBX3OPS.js.map
64
+ //# sourceMappingURL=errors-Cs0vX-mV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-Cs0vX-mV.js","names":[],"sources":["../../src/shared/errors.ts"],"sourcesContent":["export class SpeechError extends Error {\n constructor(\n message: string,\n public code: string,\n public adapter?: string,\n ) {\n super(message);\n this.name = 'SpeechError';\n }\n}\n\nexport class SpeechConfigurationError extends SpeechError {\n constructor(message: string, adapter?: string) {\n super(message, 'SPEECH_CONFIGURATION_ERROR', adapter);\n this.name = 'SpeechConfigurationError';\n }\n}\n\nexport class InvalidSpeechAdapterError extends SpeechError {\n constructor(type: string, kind: string) {\n super(`Invalid ${kind} speech adapter type: ${type}`, 'INVALID_ADAPTER');\n this.name = 'InvalidSpeechAdapterError';\n }\n}\n\nexport class SpeechProviderError extends SpeechError {\n readonly status?: number;\n readonly responseBody?: string;\n /** Parsed `Retry-After` delay in milliseconds, when the provider sent one. */\n readonly retryAfterMs?: number;\n\n constructor(\n adapter: string,\n message: string,\n options: {\n status?: number;\n responseBody?: string;\n retryAfterMs?: number;\n cause?: unknown;\n } = {},\n ) {\n super(message, 'SPEECH_PROVIDER_ERROR', adapter);\n this.name = 'SpeechProviderError';\n this.status = options.status;\n this.responseBody = options.responseBody;\n this.retryAfterMs = options.retryAfterMs;\n if (options.cause !== undefined) {\n this.cause = options.cause;\n }\n }\n}\n\n/** Why a WAV buffer was rejected by `parseWavPcm16`, or an encode input refused. */\nexport type WavFormatErrorReason =\n | 'not_riff'\n | 'truncated'\n | 'malformed_chunk'\n | 'missing_fmt'\n | 'duplicate_fmt'\n | 'data_before_fmt'\n | 'missing_data'\n | 'empty_data'\n | 'unsupported_format'\n | 'unsupported_bits'\n | 'inconsistent_header'\n | 'misaligned_data'\n | 'rate_mismatch'\n | 'channel_mismatch'\n | 'invalid_argument';\n\n/**\n * Thrown by the PCM/WAV utilities (`@happyvertical/speech/pcm`) for input that\n * is not exactly the 16-bit PCM WAV that was asked for, and for invalid\n * arguments (sample rate, sizes). `reason` is stable and machine-readable.\n */\nexport class WavFormatError extends SpeechError {\n readonly reason: WavFormatErrorReason;\n\n constructor(reason: WavFormatErrorReason, message: string) {\n super(message, 'SPEECH_INVALID_WAV');\n this.name = 'WavFormatError';\n this.reason = reason;\n }\n}\n\n/** Why a browser PCM capture could not start or produce audio. */\nexport type PcmCaptureErrorReason =\n | 'unsupported'\n | 'setup_failed'\n | 'cancelled';\n\n/** Thrown by `createPcmCapture` (`@happyvertical/speech/browser`). */\nexport class PcmCaptureError extends SpeechError {\n readonly reason: PcmCaptureErrorReason;\n\n constructor(reason: PcmCaptureErrorReason, message: string, cause?: unknown) {\n super(message, 'SPEECH_PCM_CAPTURE');\n this.name = 'PcmCaptureError';\n this.reason = reason;\n if (cause !== undefined) {\n this.cause = cause;\n }\n }\n}\n"],"mappings":";AAAA,IAAa,cAAb,cAAiC,MAAM;CAG5B;CACA;CAHT,YACE,SACA,MACA,SACA;EACA,MAAM,OAAO;EAHN,KAAA,OAAA;EACA,KAAA,UAAA;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,2BAAb,cAA8C,YAAY;CACxD,YAAY,SAAiB,SAAkB;EAC7C,MAAM,SAAS,8BAA8B,OAAO;EACpD,KAAK,OAAO;CACd;AACF;AAEA,IAAa,4BAAb,cAA+C,YAAY;CACzD,YAAY,MAAc,MAAc;EACtC,MAAM,WAAW,KAAK,wBAAwB,QAAQ,iBAAiB;EACvE,KAAK,OAAO;CACd;AACF;AAEA,IAAa,sBAAb,cAAyC,YAAY;CACnD;CACA;;CAEA;CAEA,YACE,SACA,SACA,UAKI,CAAC,GACL;EACA,MAAM,SAAS,yBAAyB,OAAO;EAC/C,KAAK,OAAO;EACZ,KAAK,SAAS,QAAQ;EACtB,KAAK,eAAe,QAAQ;EAC5B,KAAK,eAAe,QAAQ;EAC5B,IAAI,QAAQ,UAAU,KAAA,GACpB,KAAK,QAAQ,QAAQ;CAEzB;AACF;;;;;;AAyBA,IAAa,iBAAb,cAAoC,YAAY;CAC9C;CAEA,YAAY,QAA8B,SAAiB;EACzD,MAAM,SAAS,oBAAoB;EACnC,KAAK,OAAO;EACZ,KAAK,SAAS;CAChB;AACF;;AASA,IAAa,kBAAb,cAAqC,YAAY;CAC/C;CAEA,YAAY,QAA+B,SAAiB,OAAiB;EAC3E,MAAM,SAAS,oBAAoB;EACnC,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,IAAI,UAAU,KAAA,GACZ,KAAK,QAAQ;CAEjB;AACF"}
@@ -1,4 +1,4 @@
1
- import { n as SpeechConfigurationError } from "./errors-CQBX3OPS.js";
1
+ import { r as SpeechConfigurationError } from "./errors-Cs0vX-mV.js";
2
2
  //#region src/shared/audio.ts
3
3
  /**
4
4
  * Audio input normalisation shared by speech adapters: accepts the input
@@ -303,4 +303,4 @@ function registeredOptionalTranscribers() {
303
303
  //#endregion
304
304
  export { mimeTypeToAudioExtension as _, TRANSCRIBER_ENV_KEYS as a, hasTranscriberEnv as c, parseOptionalInteger as d, readEnv as f, DEFAULT_MAX_AUDIO_BYTES as g, reportSpeechUsage as h, registeredOptionalTranscribers as i, mergeHeaderInits as l, audioSecondsFromProviderUsage as m, optionalTranscriberEntry as n, defaultEnv as o, resolveTranscriberConfig as p, registerOptionalTranscriber as r, hasAnyEnv as s, getOptionalTranscriberFactory as t, parseHeadersEnv as u, normalizeAudioInput as v, normalizeMimeType as y };
305
305
 
306
- //# sourceMappingURL=registry-C_5BakmM.js.map
306
+ //# sourceMappingURL=registry-DFSF3WoM.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry-C_5BakmM.js","names":[],"sources":["../../src/shared/audio.ts","../../src/shared/usage.ts","../../src/shared/env.ts","../../src/shared/registry.ts"],"sourcesContent":["/**\n * Audio input normalisation shared by speech adapters: accepts the input\n * shapes callers actually have (Blob, Buffer, Uint8Array, ArrayBuffer,\n * ReadableStream), enforces byte limits while buffering, and derives a\n * multipart filename whose extension matches the MIME type.\n */\n\nimport { SpeechConfigurationError } from './errors.js';\nimport type { AudioInput, AudioSource } from './types.js';\n\n/** OpenAI caps `/audio/transcriptions` uploads at 25 MB. */\nexport const DEFAULT_MAX_AUDIO_BYTES = 25 * 1024 * 1024;\n\nconst MIME_EXTENSIONS: Record<string, string> = {\n 'audio/webm': 'webm',\n 'video/webm': 'webm',\n 'audio/mp4': 'm4a',\n 'audio/m4a': 'm4a',\n 'audio/x-m4a': 'm4a',\n 'video/mp4': 'mp4',\n 'audio/wav': 'wav',\n 'audio/wave': 'wav',\n 'audio/x-wav': 'wav',\n 'audio/vnd.wave': 'wav',\n 'audio/ogg': 'ogg',\n 'application/ogg': 'ogg',\n 'audio/mpeg': 'mp3',\n 'audio/mp3': 'mp3',\n 'audio/mpga': 'mp3',\n 'audio/flac': 'flac',\n 'audio/x-flac': 'flac',\n};\n\n/**\n * Strips parameters (e.g. `;codecs=opus`) and lower-cases a MIME type.\n * Returns `undefined` for blank input.\n */\nexport function normalizeMimeType(\n mimeType: string | undefined | null,\n): string | undefined {\n const essence = mimeType?.split(';')[0]?.trim().toLowerCase();\n return essence ? essence : undefined;\n}\n\n/**\n * Maps an audio MIME type to the file extension OpenAI-compatible servers use\n * to detect the container format. Codec parameters are ignored, so\n * `audio/webm;codecs=opus` maps to `webm`. Returns `undefined` when unknown.\n */\nexport function mimeTypeToAudioExtension(\n mimeType: string | undefined | null,\n): string | undefined {\n const essence = normalizeMimeType(mimeType);\n return essence ? MIME_EXTENSIONS[essence] : undefined;\n}\n\nexport interface NormalizeAudioOptions {\n /** MIME type override; wins over `AudioInput.mimeType`/`contentType` and Blob type. */\n mimeType?: string;\n /**\n * Maximum payload size in bytes. `undefined` or `Infinity` disables the\n * limit. Streams are cancelled as soon as they exceed it.\n */\n maxBytes?: number;\n /** Adapter name recorded on configuration errors. */\n adapter?: string;\n /** Aborts buffering (including a pending stream read) with the signal's reason. */\n signal?: AbortSignal;\n /** Base filename (without extension) used when the input has none. */\n defaultBasename?: string;\n /** Append a MIME-derived extension to the default filename. Default `true`. */\n deriveExtension?: boolean;\n}\n\nexport interface NormalizedAudio {\n blob: Blob;\n /** Full MIME type including parameters, or `application/octet-stream`. */\n mimeType: string;\n bytes: number;\n /** Explicit filename, or `<basename>.<ext>` derived from the MIME type. */\n filename: string;\n durationSeconds?: number;\n}\n\n/**\n * Accepts either an {@link AudioInput} wrapper or a bare {@link AudioSource}\n * and returns a size-checked Blob plus the metadata needed for multipart\n * uploads.\n */\nexport async function normalizeAudioInput(\n input: AudioInput | AudioSource,\n options: NormalizeAudioOptions = {},\n): Promise<NormalizedAudio> {\n const wrapped: AudioInput = isAudioInput(input) ? input : { data: input };\n const { adapter, signal } = options;\n signal?.throwIfAborted();\n const maxBytes = resolveMaxBytes(options.maxBytes, adapter);\n const data = wrapped.data;\n\n const blobType =\n typeof Blob !== 'undefined' && data instanceof Blob ? data.type : undefined;\n const mimeType =\n options.mimeType ??\n wrapped.mimeType ??\n wrapped.contentType ??\n (blobType || undefined) ??\n 'application/octet-stream';\n\n let blob: Blob;\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n assertWithinLimit(data.size, maxBytes, adapter);\n blob = data.type === mimeType ? data : new Blob([data], { type: mimeType });\n } else if (data instanceof Uint8Array) {\n assertWithinLimit(data.byteLength, maxBytes, adapter);\n blob = new Blob([copyBytes(data)], { type: mimeType });\n } else if (data instanceof ArrayBuffer) {\n assertWithinLimit(data.byteLength, maxBytes, adapter);\n blob = new Blob([data], { type: mimeType });\n } else if (isReadableStream(data)) {\n const chunks = await readStreamWithLimit(data, maxBytes, adapter, signal);\n blob = new Blob(chunks, { type: mimeType });\n } else {\n throw new SpeechConfigurationError(\n 'Unsupported audio input: expected Blob, Buffer, Uint8Array, ArrayBuffer, or ReadableStream',\n adapter,\n );\n }\n\n if (blob.size === 0) {\n throw new SpeechConfigurationError('Audio input is empty', adapter);\n }\n\n const extension =\n options.deriveExtension === false\n ? undefined\n : mimeTypeToAudioExtension(mimeType);\n const basename = options.defaultBasename ?? 'audio';\n const filename =\n wrapped.filename ?? (extension ? `${basename}.${extension}` : basename);\n\n return {\n blob,\n mimeType,\n bytes: blob.size,\n filename,\n durationSeconds: wrapped.durationSeconds,\n };\n}\n\nfunction isAudioInput(value: AudioInput | AudioSource): value is AudioInput {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'data' in value &&\n !(typeof Blob !== 'undefined' && value instanceof Blob) &&\n !(value instanceof Uint8Array) &&\n !(value instanceof ArrayBuffer) &&\n !isReadableStream(value)\n );\n}\n\nfunction isReadableStream(value: unknown): value is ReadableStream<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as ReadableStream).getReader === 'function'\n );\n}\n\nfunction resolveMaxBytes(\n maxBytes: number | undefined,\n adapter: string | undefined,\n): number {\n if (maxBytes === undefined) {\n return Number.POSITIVE_INFINITY;\n }\n\n if (Number.isNaN(maxBytes) || maxBytes <= 0) {\n throw new SpeechConfigurationError(\n 'maxBytes must be a positive number',\n adapter,\n );\n }\n\n return maxBytes;\n}\n\nfunction assertWithinLimit(\n bytes: number,\n maxBytes: number,\n adapter: string | undefined,\n): void {\n if (bytes > maxBytes) {\n throw new SpeechConfigurationError(\n `Audio input exceeds maxBytes (${bytes} > ${maxBytes} bytes)`,\n adapter,\n );\n }\n}\n\nasync function readStreamWithLimit(\n stream: ReadableStream<unknown>,\n maxBytes: number,\n adapter: string | undefined,\n signal: AbortSignal | undefined,\n): Promise<Uint8Array<ArrayBuffer>[]> {\n const reader = stream.getReader();\n const chunks: Uint8Array<ArrayBuffer>[] = [];\n let total = 0;\n // Cancelling the reader settles any pending read, so an abort cannot hang.\n const onAbort = () => {\n reader.cancel(signal?.reason).catch(() => undefined);\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n signal?.throwIfAborted();\n if (done) {\n break;\n }\n\n // Check the size before copying so one oversized chunk is never duplicated.\n total += chunkByteLength(value, adapter);\n if (total > maxBytes) {\n await reader.cancel().catch(() => undefined);\n throw new SpeechConfigurationError(\n `Audio stream exceeds maxBytes (more than ${maxBytes} bytes)`,\n adapter,\n );\n }\n chunks.push(toBytes(value));\n }\n } finally {\n signal?.removeEventListener('abort', onAbort);\n reader.releaseLock();\n }\n\n return chunks;\n}\n\nfunction chunkByteLength(value: unknown, adapter: string | undefined): number {\n if (value instanceof Uint8Array || value instanceof ArrayBuffer) {\n return value.byteLength;\n }\n\n throw new SpeechConfigurationError(\n 'Audio stream chunks must be Uint8Array or ArrayBuffer',\n adapter,\n );\n}\n\nfunction toBytes(value: unknown): Uint8Array<ArrayBuffer> {\n return value instanceof Uint8Array\n ? copyBytes(value)\n : new Uint8Array((value as ArrayBuffer).slice(0));\n}\n\nfunction copyBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {\n const copy = new Uint8Array(bytes.byteLength);\n copy.set(bytes);\n return copy;\n}\n","/**\n * Usage reporting shared by speech adapters.\n *\n * Adapters attach a {@link SpeechUsage} record to their results and call the\n * optional {@link SpeechUsageCallback} so callers can attribute each provider\n * call (for example to a tenant for billing).\n */\n\nexport type SpeechOperation = 'transcription' | 'synthesis';\n\nexport interface SpeechUsage {\n /** Operation that produced this usage record. */\n operation: SpeechOperation;\n /** Adapter type that made the provider call, e.g. `openai-compatible`. */\n provider: string;\n /** Model identifier sent to (or reported by) the provider. */\n model?: string;\n /** Audio duration in seconds, when the provider or caller reports it. */\n audioSeconds?: number;\n /** Size of the audio payload in bytes (uploaded for STT, received for TTS). */\n bytes?: number;\n /** Provider usage block, passed through untouched (e.g. OpenAI `usage`). */\n providerUsage?: Record<string, unknown>;\n}\n\nexport type SpeechUsageCallback = (usage: SpeechUsage) => void | Promise<void>;\n\n/**\n * Invokes every configured usage callback in order (adapter-level first, then\n * request-level). Callback errors propagate to the caller so that billing\n * hooks never fail silently.\n */\nexport async function reportSpeechUsage(\n usage: SpeechUsage,\n ...callbacks: Array<SpeechUsageCallback | undefined>\n): Promise<void> {\n for (const callback of callbacks) {\n if (callback) {\n await callback(usage);\n }\n }\n}\n\n/**\n * Extracts audio seconds from an OpenAI-style `usage` block\n * (`{ type: 'duration', seconds }`).\n */\nexport function audioSecondsFromProviderUsage(\n providerUsage: Record<string, unknown> | undefined,\n): number | undefined {\n if (!providerUsage) {\n return undefined;\n }\n\n const seconds = providerUsage.seconds;\n return typeof seconds === 'number' && Number.isFinite(seconds)\n ? seconds\n : undefined;\n}\n","/**\n * Environment configuration for speech adapters.\n *\n * Transcriber settings resolve in this order (first wins):\n * explicit options → factory context → `HAVE_SPEECH_TRANSCRIBER_*` →\n * legacy `HAVE_SPEECH_STT_*` → gateway `STT_*` aliases.\n */\n\nimport { SpeechConfigurationError } from './errors.js';\nimport type { SpeechFetch } from './types.js';\n\nexport interface SpeechEnv {\n [key: string]: string | undefined;\n}\n\n/** Environment variable names read for each transcriber setting, in priority order. */\nexport const TRANSCRIBER_ENV_KEYS = {\n type: [\n 'HAVE_SPEECH_TRANSCRIBER_TYPE',\n 'HAVE_SPEECH_STT_TYPE',\n 'HAVE_SPEECH_STT_ADAPTER',\n 'STT_ADAPTER',\n ],\n baseUrl: [\n 'HAVE_SPEECH_TRANSCRIBER_BASE_URL',\n 'HAVE_SPEECH_STT_BASE_URL',\n 'STT_BASE_URL',\n ],\n model: [\n 'HAVE_SPEECH_TRANSCRIBER_MODEL',\n 'HAVE_SPEECH_STT_MODEL',\n 'STT_MODEL',\n ],\n apiKey: [\n 'HAVE_SPEECH_TRANSCRIBER_API_KEY',\n 'HAVE_SPEECH_STT_API_KEY',\n 'STT_API_KEY',\n 'SPEECH_API_KEY',\n ],\n timeoutMs: [\n 'HAVE_SPEECH_TRANSCRIBER_TIMEOUT',\n 'HAVE_SPEECH_TRANSCRIBER_TIMEOUT_MS',\n 'HAVE_SPEECH_STT_TIMEOUT_MS',\n 'STT_TIMEOUT_MS',\n ],\n maxBytes: ['HAVE_SPEECH_TRANSCRIBER_MAX_BYTES', 'STT_MAX_BYTES'],\n headers: ['HAVE_SPEECH_TRANSCRIBER_HEADERS', 'STT_HEADERS'],\n path: ['HAVE_SPEECH_TRANSCRIBER_PATH', 'HAVE_SPEECH_STT_PATH', 'STT_PATH'],\n} as const;\n\n/** Transcriber settings read from the environment. All fields are optional. */\nexport interface TranscriberEnvConfig {\n type?: string;\n baseUrl?: string;\n model?: string;\n apiKey?: string;\n timeoutMs?: number;\n maxBytes?: number;\n headers?: Record<string, string>;\n path?: string;\n}\n\n/** Options shape accepted by {@link resolveTranscriberConfig}. */\nexport interface TranscriberConfigInput {\n type?: string;\n baseUrl?: string;\n model?: string;\n apiKey?: string;\n timeoutMs?: number;\n maxBytes?: number;\n headers?: HeadersInit;\n fetch?: SpeechFetch;\n}\n\nexport interface TranscriberConfigContext {\n env?: SpeechEnv;\n fetch?: SpeechFetch;\n headers?: HeadersInit;\n}\n\n/** Resolved transcriber settings. `headers` merges env → context → options. */\nexport interface ResolvedTranscriberConfig {\n type?: string;\n baseUrl?: string;\n model?: string;\n apiKey?: string;\n timeoutMs?: number;\n maxBytes?: number;\n headers?: Headers;\n fetch?: SpeechFetch;\n path?: string;\n}\n\nexport function readTranscriberEnv(env: SpeechEnv): TranscriberEnvConfig {\n return {\n type: readEnv(env, ...TRANSCRIBER_ENV_KEYS.type),\n baseUrl: readEnv(env, ...TRANSCRIBER_ENV_KEYS.baseUrl),\n model: readEnv(env, ...TRANSCRIBER_ENV_KEYS.model),\n apiKey: readEnv(env, ...TRANSCRIBER_ENV_KEYS.apiKey),\n timeoutMs: parseOptionalInteger(\n readEnv(env, ...TRANSCRIBER_ENV_KEYS.timeoutMs),\n ),\n maxBytes: parseOptionalInteger(\n readEnv(env, ...TRANSCRIBER_ENV_KEYS.maxBytes),\n ),\n headers: parseHeadersEnv(env, TRANSCRIBER_ENV_KEYS.headers),\n path: readEnv(env, ...TRANSCRIBER_ENV_KEYS.path),\n };\n}\n\nexport function hasTranscriberEnv(env: SpeechEnv): boolean {\n return hasAnyEnv(\n env,\n ...TRANSCRIBER_ENV_KEYS.type,\n ...TRANSCRIBER_ENV_KEYS.baseUrl,\n );\n}\n\n/**\n * Merges explicit transcriber options with the factory context and the\n * environment. Explicit options always win; headers are merged so a\n * per-deployment env header (e.g. a gateway virtual key) can be combined with\n * explicit ones.\n */\nexport function resolveTranscriberConfig(\n options: TranscriberConfigInput = {},\n context: TranscriberConfigContext = {},\n): ResolvedTranscriberConfig {\n const fromEnv = readTranscriberEnv(context.env ?? defaultEnv());\n const headers = mergeHeaderInits(\n fromEnv.headers,\n context.headers,\n options.headers,\n );\n\n return {\n type: options.type ?? fromEnv.type,\n baseUrl: (options.baseUrl ?? fromEnv.baseUrl)?.trim(),\n model: options.model ?? fromEnv.model,\n apiKey: options.apiKey ?? fromEnv.apiKey,\n timeoutMs: options.timeoutMs ?? fromEnv.timeoutMs,\n maxBytes: options.maxBytes ?? fromEnv.maxBytes,\n headers,\n fetch: options.fetch ?? context.fetch,\n path: fromEnv.path,\n };\n}\n\n/** Merges header sets left to right; later sets win per header name. */\nexport function mergeHeaderInits(\n ...sets: Array<HeadersInit | undefined>\n): Headers | undefined {\n const present = sets.filter((set): set is HeadersInit => Boolean(set));\n if (present.length === 0) {\n return undefined;\n }\n\n const merged = new Headers();\n for (const set of present) {\n new Headers(set).forEach((value, key) => {\n merged.set(key, value);\n });\n }\n return merged;\n}\n\nexport function readEnv(env: SpeechEnv, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const value = env[key];\n if (value?.trim()) {\n return value.trim();\n }\n }\n\n return undefined;\n}\n\nexport function hasAnyEnv(env: SpeechEnv, ...keys: string[]): boolean {\n return keys.some((key) => Boolean(env[key]?.trim()));\n}\n\nexport function parseOptionalInteger(\n value: string | undefined,\n): number | undefined {\n if (!value) {\n return undefined;\n }\n\n const parsed = Number.parseInt(value, 10);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nexport function defaultEnv(): SpeechEnv {\n return globalThis.process?.env ?? {};\n}\n\n/**\n * Parses a JSON object of header names to string values. The raw value is\n * never echoed in errors because it may carry credentials.\n */\nexport function parseHeadersEnv(\n env: SpeechEnv,\n keys: readonly string[],\n): Record<string, string> | undefined {\n for (const key of keys) {\n const raw = env[key]?.trim();\n if (!raw) {\n continue;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new SpeechConfigurationError(\n `${key} must be a JSON object of header names to string values`,\n );\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n Array.isArray(parsed) ||\n !Object.values(parsed).every((value) => typeof value === 'string')\n ) {\n throw new SpeechConfigurationError(\n `${key} must be a JSON object of header names to string values`,\n );\n }\n\n return parsed as Record<string, string>;\n }\n\n return undefined;\n}\n","/**\n * Registry for opt-in transcriber adapters that live behind subpath exports\n * (e.g. `@happyvertical/speech/local`).\n *\n * The core entry never imports those adapters, so their heavy optional peer\n * dependencies are never resolved by bundlers or loaded at runtime unless the\n * caller imports the subpath, which registers its factory here.\n */\n\nimport type { SpeechFactoryContext } from './factory.js';\nimport type {\n GetTranscriberOptions,\n Transcriber,\n TranscriberType,\n} from './types.js';\n\nexport type OptionalTranscriberFactory = (\n options: GetTranscriberOptions,\n context: SpeechFactoryContext,\n) => Transcriber | Promise<Transcriber>;\n\n/** Adapter types that are only available after importing a subpath. */\nconst OPTIONAL_TRANSCRIBER_ENTRIES: Partial<Record<TranscriberType, string>> = {\n local: '@happyvertical/speech/local',\n};\n\nconst registered = new Map<TranscriberType, OptionalTranscriberFactory>();\n\nexport function registerOptionalTranscriber(\n type: TranscriberType,\n factory: OptionalTranscriberFactory,\n): void {\n registered.set(type, factory);\n}\n\nexport function getOptionalTranscriberFactory(\n type: string,\n): OptionalTranscriberFactory | undefined {\n return registered.get(type as TranscriberType);\n}\n\n/** Subpath that registers `type`, when it is an opt-in adapter. */\nexport function optionalTranscriberEntry(type: string): string | undefined {\n return OPTIONAL_TRANSCRIBER_ENTRIES[type as TranscriberType];\n}\n\nexport function registeredOptionalTranscribers(): TranscriberType[] {\n return [...registered.keys()];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"registry-DFSF3WoM.js","names":[],"sources":["../../src/shared/audio.ts","../../src/shared/usage.ts","../../src/shared/env.ts","../../src/shared/registry.ts"],"sourcesContent":["/**\n * Audio input normalisation shared by speech adapters: accepts the input\n * shapes callers actually have (Blob, Buffer, Uint8Array, ArrayBuffer,\n * ReadableStream), enforces byte limits while buffering, and derives a\n * multipart filename whose extension matches the MIME type.\n */\n\nimport { SpeechConfigurationError } from './errors.js';\nimport type { AudioInput, AudioSource } from './types.js';\n\n/** OpenAI caps `/audio/transcriptions` uploads at 25 MB. */\nexport const DEFAULT_MAX_AUDIO_BYTES = 25 * 1024 * 1024;\n\nconst MIME_EXTENSIONS: Record<string, string> = {\n 'audio/webm': 'webm',\n 'video/webm': 'webm',\n 'audio/mp4': 'm4a',\n 'audio/m4a': 'm4a',\n 'audio/x-m4a': 'm4a',\n 'video/mp4': 'mp4',\n 'audio/wav': 'wav',\n 'audio/wave': 'wav',\n 'audio/x-wav': 'wav',\n 'audio/vnd.wave': 'wav',\n 'audio/ogg': 'ogg',\n 'application/ogg': 'ogg',\n 'audio/mpeg': 'mp3',\n 'audio/mp3': 'mp3',\n 'audio/mpga': 'mp3',\n 'audio/flac': 'flac',\n 'audio/x-flac': 'flac',\n};\n\n/**\n * Strips parameters (e.g. `;codecs=opus`) and lower-cases a MIME type.\n * Returns `undefined` for blank input.\n */\nexport function normalizeMimeType(\n mimeType: string | undefined | null,\n): string | undefined {\n const essence = mimeType?.split(';')[0]?.trim().toLowerCase();\n return essence ? essence : undefined;\n}\n\n/**\n * Maps an audio MIME type to the file extension OpenAI-compatible servers use\n * to detect the container format. Codec parameters are ignored, so\n * `audio/webm;codecs=opus` maps to `webm`. Returns `undefined` when unknown.\n */\nexport function mimeTypeToAudioExtension(\n mimeType: string | undefined | null,\n): string | undefined {\n const essence = normalizeMimeType(mimeType);\n return essence ? MIME_EXTENSIONS[essence] : undefined;\n}\n\nexport interface NormalizeAudioOptions {\n /** MIME type override; wins over `AudioInput.mimeType`/`contentType` and Blob type. */\n mimeType?: string;\n /**\n * Maximum payload size in bytes. `undefined` or `Infinity` disables the\n * limit. Streams are cancelled as soon as they exceed it.\n */\n maxBytes?: number;\n /** Adapter name recorded on configuration errors. */\n adapter?: string;\n /** Aborts buffering (including a pending stream read) with the signal's reason. */\n signal?: AbortSignal;\n /** Base filename (without extension) used when the input has none. */\n defaultBasename?: string;\n /** Append a MIME-derived extension to the default filename. Default `true`. */\n deriveExtension?: boolean;\n}\n\nexport interface NormalizedAudio {\n blob: Blob;\n /** Full MIME type including parameters, or `application/octet-stream`. */\n mimeType: string;\n bytes: number;\n /** Explicit filename, or `<basename>.<ext>` derived from the MIME type. */\n filename: string;\n durationSeconds?: number;\n}\n\n/**\n * Accepts either an {@link AudioInput} wrapper or a bare {@link AudioSource}\n * and returns a size-checked Blob plus the metadata needed for multipart\n * uploads.\n */\nexport async function normalizeAudioInput(\n input: AudioInput | AudioSource,\n options: NormalizeAudioOptions = {},\n): Promise<NormalizedAudio> {\n const wrapped: AudioInput = isAudioInput(input) ? input : { data: input };\n const { adapter, signal } = options;\n signal?.throwIfAborted();\n const maxBytes = resolveMaxBytes(options.maxBytes, adapter);\n const data = wrapped.data;\n\n const blobType =\n typeof Blob !== 'undefined' && data instanceof Blob ? data.type : undefined;\n const mimeType =\n options.mimeType ??\n wrapped.mimeType ??\n wrapped.contentType ??\n (blobType || undefined) ??\n 'application/octet-stream';\n\n let blob: Blob;\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n assertWithinLimit(data.size, maxBytes, adapter);\n blob = data.type === mimeType ? data : new Blob([data], { type: mimeType });\n } else if (data instanceof Uint8Array) {\n assertWithinLimit(data.byteLength, maxBytes, adapter);\n blob = new Blob([copyBytes(data)], { type: mimeType });\n } else if (data instanceof ArrayBuffer) {\n assertWithinLimit(data.byteLength, maxBytes, adapter);\n blob = new Blob([data], { type: mimeType });\n } else if (isReadableStream(data)) {\n const chunks = await readStreamWithLimit(data, maxBytes, adapter, signal);\n blob = new Blob(chunks, { type: mimeType });\n } else {\n throw new SpeechConfigurationError(\n 'Unsupported audio input: expected Blob, Buffer, Uint8Array, ArrayBuffer, or ReadableStream',\n adapter,\n );\n }\n\n if (blob.size === 0) {\n throw new SpeechConfigurationError('Audio input is empty', adapter);\n }\n\n const extension =\n options.deriveExtension === false\n ? undefined\n : mimeTypeToAudioExtension(mimeType);\n const basename = options.defaultBasename ?? 'audio';\n const filename =\n wrapped.filename ?? (extension ? `${basename}.${extension}` : basename);\n\n return {\n blob,\n mimeType,\n bytes: blob.size,\n filename,\n durationSeconds: wrapped.durationSeconds,\n };\n}\n\nfunction isAudioInput(value: AudioInput | AudioSource): value is AudioInput {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'data' in value &&\n !(typeof Blob !== 'undefined' && value instanceof Blob) &&\n !(value instanceof Uint8Array) &&\n !(value instanceof ArrayBuffer) &&\n !isReadableStream(value)\n );\n}\n\nfunction isReadableStream(value: unknown): value is ReadableStream<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as ReadableStream).getReader === 'function'\n );\n}\n\nfunction resolveMaxBytes(\n maxBytes: number | undefined,\n adapter: string | undefined,\n): number {\n if (maxBytes === undefined) {\n return Number.POSITIVE_INFINITY;\n }\n\n if (Number.isNaN(maxBytes) || maxBytes <= 0) {\n throw new SpeechConfigurationError(\n 'maxBytes must be a positive number',\n adapter,\n );\n }\n\n return maxBytes;\n}\n\nfunction assertWithinLimit(\n bytes: number,\n maxBytes: number,\n adapter: string | undefined,\n): void {\n if (bytes > maxBytes) {\n throw new SpeechConfigurationError(\n `Audio input exceeds maxBytes (${bytes} > ${maxBytes} bytes)`,\n adapter,\n );\n }\n}\n\nasync function readStreamWithLimit(\n stream: ReadableStream<unknown>,\n maxBytes: number,\n adapter: string | undefined,\n signal: AbortSignal | undefined,\n): Promise<Uint8Array<ArrayBuffer>[]> {\n const reader = stream.getReader();\n const chunks: Uint8Array<ArrayBuffer>[] = [];\n let total = 0;\n // Cancelling the reader settles any pending read, so an abort cannot hang.\n const onAbort = () => {\n reader.cancel(signal?.reason).catch(() => undefined);\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n signal?.throwIfAborted();\n if (done) {\n break;\n }\n\n // Check the size before copying so one oversized chunk is never duplicated.\n total += chunkByteLength(value, adapter);\n if (total > maxBytes) {\n await reader.cancel().catch(() => undefined);\n throw new SpeechConfigurationError(\n `Audio stream exceeds maxBytes (more than ${maxBytes} bytes)`,\n adapter,\n );\n }\n chunks.push(toBytes(value));\n }\n } finally {\n signal?.removeEventListener('abort', onAbort);\n reader.releaseLock();\n }\n\n return chunks;\n}\n\nfunction chunkByteLength(value: unknown, adapter: string | undefined): number {\n if (value instanceof Uint8Array || value instanceof ArrayBuffer) {\n return value.byteLength;\n }\n\n throw new SpeechConfigurationError(\n 'Audio stream chunks must be Uint8Array or ArrayBuffer',\n adapter,\n );\n}\n\nfunction toBytes(value: unknown): Uint8Array<ArrayBuffer> {\n return value instanceof Uint8Array\n ? copyBytes(value)\n : new Uint8Array((value as ArrayBuffer).slice(0));\n}\n\nfunction copyBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {\n const copy = new Uint8Array(bytes.byteLength);\n copy.set(bytes);\n return copy;\n}\n","/**\n * Usage reporting shared by speech adapters.\n *\n * Adapters attach a {@link SpeechUsage} record to their results and call the\n * optional {@link SpeechUsageCallback} so callers can attribute each provider\n * call (for example to a tenant for billing).\n */\n\nexport type SpeechOperation = 'transcription' | 'synthesis';\n\nexport interface SpeechUsage {\n /** Operation that produced this usage record. */\n operation: SpeechOperation;\n /** Adapter type that made the provider call, e.g. `openai-compatible`. */\n provider: string;\n /** Model identifier sent to (or reported by) the provider. */\n model?: string;\n /** Audio duration in seconds, when the provider or caller reports it. */\n audioSeconds?: number;\n /** Size of the audio payload in bytes (uploaded for STT, received for TTS). */\n bytes?: number;\n /** Provider usage block, passed through untouched (e.g. OpenAI `usage`). */\n providerUsage?: Record<string, unknown>;\n}\n\nexport type SpeechUsageCallback = (usage: SpeechUsage) => void | Promise<void>;\n\n/**\n * Invokes every configured usage callback in order (adapter-level first, then\n * request-level). Callback errors propagate to the caller so that billing\n * hooks never fail silently.\n */\nexport async function reportSpeechUsage(\n usage: SpeechUsage,\n ...callbacks: Array<SpeechUsageCallback | undefined>\n): Promise<void> {\n for (const callback of callbacks) {\n if (callback) {\n await callback(usage);\n }\n }\n}\n\n/**\n * Extracts audio seconds from an OpenAI-style `usage` block\n * (`{ type: 'duration', seconds }`).\n */\nexport function audioSecondsFromProviderUsage(\n providerUsage: Record<string, unknown> | undefined,\n): number | undefined {\n if (!providerUsage) {\n return undefined;\n }\n\n const seconds = providerUsage.seconds;\n return typeof seconds === 'number' && Number.isFinite(seconds)\n ? seconds\n : undefined;\n}\n","/**\n * Environment configuration for speech adapters.\n *\n * Transcriber settings resolve in this order (first wins):\n * explicit options → factory context → `HAVE_SPEECH_TRANSCRIBER_*` →\n * legacy `HAVE_SPEECH_STT_*` → gateway `STT_*` aliases.\n */\n\nimport { SpeechConfigurationError } from './errors.js';\nimport type { SpeechFetch } from './types.js';\n\nexport interface SpeechEnv {\n [key: string]: string | undefined;\n}\n\n/** Environment variable names read for each transcriber setting, in priority order. */\nexport const TRANSCRIBER_ENV_KEYS = {\n type: [\n 'HAVE_SPEECH_TRANSCRIBER_TYPE',\n 'HAVE_SPEECH_STT_TYPE',\n 'HAVE_SPEECH_STT_ADAPTER',\n 'STT_ADAPTER',\n ],\n baseUrl: [\n 'HAVE_SPEECH_TRANSCRIBER_BASE_URL',\n 'HAVE_SPEECH_STT_BASE_URL',\n 'STT_BASE_URL',\n ],\n model: [\n 'HAVE_SPEECH_TRANSCRIBER_MODEL',\n 'HAVE_SPEECH_STT_MODEL',\n 'STT_MODEL',\n ],\n apiKey: [\n 'HAVE_SPEECH_TRANSCRIBER_API_KEY',\n 'HAVE_SPEECH_STT_API_KEY',\n 'STT_API_KEY',\n 'SPEECH_API_KEY',\n ],\n timeoutMs: [\n 'HAVE_SPEECH_TRANSCRIBER_TIMEOUT',\n 'HAVE_SPEECH_TRANSCRIBER_TIMEOUT_MS',\n 'HAVE_SPEECH_STT_TIMEOUT_MS',\n 'STT_TIMEOUT_MS',\n ],\n maxBytes: ['HAVE_SPEECH_TRANSCRIBER_MAX_BYTES', 'STT_MAX_BYTES'],\n headers: ['HAVE_SPEECH_TRANSCRIBER_HEADERS', 'STT_HEADERS'],\n path: ['HAVE_SPEECH_TRANSCRIBER_PATH', 'HAVE_SPEECH_STT_PATH', 'STT_PATH'],\n} as const;\n\n/** Transcriber settings read from the environment. All fields are optional. */\nexport interface TranscriberEnvConfig {\n type?: string;\n baseUrl?: string;\n model?: string;\n apiKey?: string;\n timeoutMs?: number;\n maxBytes?: number;\n headers?: Record<string, string>;\n path?: string;\n}\n\n/** Options shape accepted by {@link resolveTranscriberConfig}. */\nexport interface TranscriberConfigInput {\n type?: string;\n baseUrl?: string;\n model?: string;\n apiKey?: string;\n timeoutMs?: number;\n maxBytes?: number;\n headers?: HeadersInit;\n fetch?: SpeechFetch;\n}\n\nexport interface TranscriberConfigContext {\n env?: SpeechEnv;\n fetch?: SpeechFetch;\n headers?: HeadersInit;\n}\n\n/** Resolved transcriber settings. `headers` merges env → context → options. */\nexport interface ResolvedTranscriberConfig {\n type?: string;\n baseUrl?: string;\n model?: string;\n apiKey?: string;\n timeoutMs?: number;\n maxBytes?: number;\n headers?: Headers;\n fetch?: SpeechFetch;\n path?: string;\n}\n\nexport function readTranscriberEnv(env: SpeechEnv): TranscriberEnvConfig {\n return {\n type: readEnv(env, ...TRANSCRIBER_ENV_KEYS.type),\n baseUrl: readEnv(env, ...TRANSCRIBER_ENV_KEYS.baseUrl),\n model: readEnv(env, ...TRANSCRIBER_ENV_KEYS.model),\n apiKey: readEnv(env, ...TRANSCRIBER_ENV_KEYS.apiKey),\n timeoutMs: parseOptionalInteger(\n readEnv(env, ...TRANSCRIBER_ENV_KEYS.timeoutMs),\n ),\n maxBytes: parseOptionalInteger(\n readEnv(env, ...TRANSCRIBER_ENV_KEYS.maxBytes),\n ),\n headers: parseHeadersEnv(env, TRANSCRIBER_ENV_KEYS.headers),\n path: readEnv(env, ...TRANSCRIBER_ENV_KEYS.path),\n };\n}\n\nexport function hasTranscriberEnv(env: SpeechEnv): boolean {\n return hasAnyEnv(\n env,\n ...TRANSCRIBER_ENV_KEYS.type,\n ...TRANSCRIBER_ENV_KEYS.baseUrl,\n );\n}\n\n/**\n * Merges explicit transcriber options with the factory context and the\n * environment. Explicit options always win; headers are merged so a\n * per-deployment env header (e.g. a gateway virtual key) can be combined with\n * explicit ones.\n */\nexport function resolveTranscriberConfig(\n options: TranscriberConfigInput = {},\n context: TranscriberConfigContext = {},\n): ResolvedTranscriberConfig {\n const fromEnv = readTranscriberEnv(context.env ?? defaultEnv());\n const headers = mergeHeaderInits(\n fromEnv.headers,\n context.headers,\n options.headers,\n );\n\n return {\n type: options.type ?? fromEnv.type,\n baseUrl: (options.baseUrl ?? fromEnv.baseUrl)?.trim(),\n model: options.model ?? fromEnv.model,\n apiKey: options.apiKey ?? fromEnv.apiKey,\n timeoutMs: options.timeoutMs ?? fromEnv.timeoutMs,\n maxBytes: options.maxBytes ?? fromEnv.maxBytes,\n headers,\n fetch: options.fetch ?? context.fetch,\n path: fromEnv.path,\n };\n}\n\n/** Merges header sets left to right; later sets win per header name. */\nexport function mergeHeaderInits(\n ...sets: Array<HeadersInit | undefined>\n): Headers | undefined {\n const present = sets.filter((set): set is HeadersInit => Boolean(set));\n if (present.length === 0) {\n return undefined;\n }\n\n const merged = new Headers();\n for (const set of present) {\n new Headers(set).forEach((value, key) => {\n merged.set(key, value);\n });\n }\n return merged;\n}\n\nexport function readEnv(env: SpeechEnv, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const value = env[key];\n if (value?.trim()) {\n return value.trim();\n }\n }\n\n return undefined;\n}\n\nexport function hasAnyEnv(env: SpeechEnv, ...keys: string[]): boolean {\n return keys.some((key) => Boolean(env[key]?.trim()));\n}\n\nexport function parseOptionalInteger(\n value: string | undefined,\n): number | undefined {\n if (!value) {\n return undefined;\n }\n\n const parsed = Number.parseInt(value, 10);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nexport function defaultEnv(): SpeechEnv {\n return globalThis.process?.env ?? {};\n}\n\n/**\n * Parses a JSON object of header names to string values. The raw value is\n * never echoed in errors because it may carry credentials.\n */\nexport function parseHeadersEnv(\n env: SpeechEnv,\n keys: readonly string[],\n): Record<string, string> | undefined {\n for (const key of keys) {\n const raw = env[key]?.trim();\n if (!raw) {\n continue;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new SpeechConfigurationError(\n `${key} must be a JSON object of header names to string values`,\n );\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n Array.isArray(parsed) ||\n !Object.values(parsed).every((value) => typeof value === 'string')\n ) {\n throw new SpeechConfigurationError(\n `${key} must be a JSON object of header names to string values`,\n );\n }\n\n return parsed as Record<string, string>;\n }\n\n return undefined;\n}\n","/**\n * Registry for opt-in transcriber adapters that live behind subpath exports\n * (e.g. `@happyvertical/speech/local`).\n *\n * The core entry never imports those adapters, so their heavy optional peer\n * dependencies are never resolved by bundlers or loaded at runtime unless the\n * caller imports the subpath, which registers its factory here.\n */\n\nimport type { SpeechFactoryContext } from './factory.js';\nimport type {\n GetTranscriberOptions,\n Transcriber,\n TranscriberType,\n} from './types.js';\n\nexport type OptionalTranscriberFactory = (\n options: GetTranscriberOptions,\n context: SpeechFactoryContext,\n) => Transcriber | Promise<Transcriber>;\n\n/** Adapter types that are only available after importing a subpath. */\nconst OPTIONAL_TRANSCRIBER_ENTRIES: Partial<Record<TranscriberType, string>> = {\n local: '@happyvertical/speech/local',\n};\n\nconst registered = new Map<TranscriberType, OptionalTranscriberFactory>();\n\nexport function registerOptionalTranscriber(\n type: TranscriberType,\n factory: OptionalTranscriberFactory,\n): void {\n registered.set(type, factory);\n}\n\nexport function getOptionalTranscriberFactory(\n type: string,\n): OptionalTranscriberFactory | undefined {\n return registered.get(type as TranscriberType);\n}\n\n/** Subpath that registers `type`, when it is an opt-in adapter. */\nexport function optionalTranscriberEntry(type: string): string | undefined {\n return OPTIONAL_TRANSCRIBER_ENTRIES[type as TranscriberType];\n}\n\nexport function registeredOptionalTranscribers(): TranscriberType[] {\n return [...registered.keys()];\n}\n"],"mappings":"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"}