@happyvertical/speech 0.98.0 → 0.99.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.
- package/AGENT.md +3 -1
- package/README.md +33 -0
- package/dist/chunks/errors-CQBX3OPS.js +41 -0
- package/dist/chunks/errors-CQBX3OPS.js.map +1 -0
- package/dist/chunks/{registry-DfID41Ew.js → registry-C_5BakmM.js} +3 -40
- package/dist/chunks/registry-C_5BakmM.js.map +1 -0
- package/dist/chunks/websocket-CR9uXAuS.js +44 -0
- package/dist/chunks/websocket-CR9uXAuS.js.map +1 -0
- package/dist/conversation/openai-webrtc.d.ts +4 -0
- package/dist/conversation/openai-webrtc.d.ts.map +1 -0
- package/dist/conversation/types.d.ts +60 -0
- package/dist/conversation/types.d.ts.map +1 -0
- package/dist/conversation-server.d.ts +40 -0
- package/dist/conversation-server.d.ts.map +1 -0
- package/dist/conversation-server.js +174 -0
- package/dist/conversation-server.js.map +1 -0
- package/dist/conversation.d.ts +4 -0
- package/dist/conversation.d.ts.map +1 -0
- package/dist/conversation.js +346 -0
- package/dist/conversation.js.map +1 -0
- package/dist/index.js +3 -41
- package/dist/index.js.map +1 -1
- package/dist/local.js +2 -1
- package/dist/local.js.map +1 -1
- package/metadata.json +3 -1
- package/package.json +9 -1
- package/dist/chunks/registry-DfID41Ew.js.map +0 -1
package/AGENT.md
CHANGED
|
@@ -27,7 +27,7 @@ pnpm --filter @happyvertical/speech clean
|
|
|
27
27
|
|
|
28
28
|
## Ecosystem Relationships
|
|
29
29
|
- Provides: Speech provider abstraction for STT and TTS backends
|
|
30
|
-
- Implements: Studio Server STT, OpenAI-compatible STT, OpenAI Realtime streaming STT, Voxtral Realtime streaming STT, On-device STT, Studio Server TTS, Qwen3 TTS, OpenAI-compatible TTS
|
|
30
|
+
- Implements: Studio Server STT, OpenAI-compatible STT, OpenAI Realtime streaming STT, Voxtral Realtime streaming STT, On-device STT, Studio Server TTS, Qwen3 TTS, OpenAI-compatible TTS, OpenAI WebRTC conversational voice, Server call creation/termination
|
|
31
31
|
- Requires: @huggingface/transformers
|
|
32
32
|
- Stability: experimental (Marked as preview or experimental in package guidance.)
|
|
33
33
|
<!-- END AGENT:GENERATED -->
|
|
@@ -54,6 +54,8 @@ Adapter constructors are internal implementation details. Keep new backends behi
|
|
|
54
54
|
- Qwen3 TTS (`type: 'qwen3-tts'`) posts multipart form data to `/v1/audio/speech`.
|
|
55
55
|
- OpenAI-compatible TTS (`type: 'openai-compatible'`) posts OpenAI-shaped JSON to `<base>/audio/speech`, resolved like the transcriber (`resolveOpenAICompatibleUrl`: server root → `/v1/audio/speech`, `/vN` root → `/audio/speech`, full endpoint as-is; an explicit `speechPath` resolves relative to `baseUrl` instead).
|
|
56
56
|
|
|
57
|
+
- OpenAI WebRTC conversational voice (`createOpenAIWebRTCVoiceSession`) uses the optional `@happyvertical/speech/conversation` entry for audio input/output, typed turns, transcripts and interruption. Server call creation/termination is isolated in `@happyvertical/speech/conversation/server`; hosts own credentials and admission policy.
|
|
58
|
+
|
|
57
59
|
## Shared Building Blocks
|
|
58
60
|
|
|
59
61
|
New adapters (including streaming/realtime transcribers) should reuse these modules in `src/shared/` instead of re-implementing them:
|
package/README.md
CHANGED
|
@@ -415,3 +415,36 @@ HV_SPEECH_MODEL_TESTS=1 pnpm --filter @happyvertical/speech test local-transcrib
|
|
|
415
415
|
Docker or Testcontainers integration suites should run the fixture services or Studio Server with mock backends. Model-backed tests must remain opt-in, for example behind `HV_SPEECH_MODEL_TESTS=1`, so the normal SDK suite never downloads model weights.
|
|
416
416
|
|
|
417
417
|
On Apple Silicon, run model-backed Qwen tests with a host-native Metal/MLX runtime or against a remote cluster service; Docker contract tests should continue using mock backends because Linux containers do not expose the host Metal runtime.
|
|
418
|
+
|
|
419
|
+
## Conversational voice (OpenAI WebRTC)
|
|
420
|
+
|
|
421
|
+
`@happyvertical/speech/conversation` is an optional browser-safe speech-to-speech session API. It is separate from `getStreamingTranscriber()`: transcription sessions recognize audio, while conversational sessions also generate replies and audio. Existing STT/TTS entry points remain compatible.
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { createOpenAIWebRTCVoiceSession } from '@happyvertical/speech/conversation';
|
|
425
|
+
|
|
426
|
+
const voice = createOpenAIWebRTCVoiceSession({
|
|
427
|
+
getMicrophone: () => navigator.mediaDevices.getUserMedia({ audio: true }),
|
|
428
|
+
negotiate: async (offer, signal) => {
|
|
429
|
+
const response = await fetch('/api/voice/call', { method: 'POST', body: offer, signal });
|
|
430
|
+
if (!response.ok) throw new Error('Voice unavailable');
|
|
431
|
+
return response.text();
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
voice.on('transcript', (turn) => renderTurn(turn)); // upsert by itemId
|
|
435
|
+
voice.on('speaking', (active) => animatePlayback(active));
|
|
436
|
+
await voice.connect();
|
|
437
|
+
voice.sendText('What can you help me with?');
|
|
438
|
+
// voice.interrupt(); voice.setMicMuted(true); voice.setOutputMuted(true);
|
|
439
|
+
voice.close();
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The host server imports `createOpenAIVoiceCall` and `hangupOpenAIVoiceCall` from `@happyvertical/speech/conversation/server`. Call creation accepts the SDP offer and a server-owned model, voice (default `marin`), instructions, optional transcription model, turn detection and tools. It returns `{ answer, callId }`; return only the answer to the browser and retain the call id for termination. The browser never receives a long-lived provider key. Optional server helpers are not reachable from the browser conversation entry or the core entry.
|
|
443
|
+
|
|
444
|
+
The host owns authorization, tenant/session attribution, origin checks, request/admission limits, idle/hard timeouts and server-side termination. Configuration sent at creation is not an immutable authorization boundary: an untrusted WebRTC client can send protocol events. Use provider spend controls and server-side monitoring when enforcing call policy. Never expose privileged tools to an anonymous client. Creating a call is not automatically retried because a retry may create another billable session. Setup failures after call identification trigger hangup; if that cleanup also fails, `VoiceCallSetupError.callId` provides the server recovery handle. Keep its admission slot reserved and retry termination.
|
|
445
|
+
|
|
446
|
+
A session takes ownership of the stream returned by `getMicrophone`, including stopping tracks returned after cancellation. `connect()` coalesces pending requests; closed/failed sessions are terminal. `close()` releases playback, tracks, peer/data channel and listeners. The host must separately end its server call lease, including on client negotiation failure. Connection timeout includes permission acquisition; a late permission grant is cleaned up. Microphone track termination (revoked permission or a lost input device) and unexpected channel/peer loss fail the session; create a new session and explicitly restore only completed history rather than replaying unacknowledged audio.
|
|
447
|
+
|
|
448
|
+
Transcript events contain `itemId`, `role`, full accumulated `text` and `final`. Final transcripts are deduplicated. Input transcription is an optional asynchronous side channel and does not drive model replies. `speaking` follows WebRTC output-buffer playback events, while `response` describes generation. Muting input disables tracks and clears buffered input; muting output silences playback independently. `commitInput()` commits microphone audio and requests a response when turn detection is manual (`commitInput(false)` commits without a reply). `interrupt()` cancels active generation and clears queued playback; OpenAI WebRTC handles interruption history truncation. Submit every function output with `submitToolResult`, then call `respond()` once after generation has finished and all tools have resolved; output submission does not create a reply. Incomplete replies and failed input transcription also emit recoverable `error` events without provider payloads. Recoverable provider/playback errors emit `error`; callers should show an actionable status. No raw audio is retained by this adapter.
|
|
449
|
+
|
|
450
|
+
Protocol reference: [OpenAI WebRTC](https://developers.openai.com/api/docs/guides/voice-webrtc), [server controls](https://developers.openai.com/api/docs/guides/voice-server-controls). Real media behavior still requires browser QA; deterministic transport tests do not prove microphone permissions or echo cancellation.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/shared/errors.ts
|
|
2
|
+
var SpeechError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
adapter;
|
|
5
|
+
constructor(message, code, adapter) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.adapter = adapter;
|
|
9
|
+
this.name = "SpeechError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var SpeechConfigurationError = class extends SpeechError {
|
|
13
|
+
constructor(message, adapter) {
|
|
14
|
+
super(message, "SPEECH_CONFIGURATION_ERROR", adapter);
|
|
15
|
+
this.name = "SpeechConfigurationError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var InvalidSpeechAdapterError = class extends SpeechError {
|
|
19
|
+
constructor(type, kind) {
|
|
20
|
+
super(`Invalid ${kind} speech adapter type: ${type}`, "INVALID_ADAPTER");
|
|
21
|
+
this.name = "InvalidSpeechAdapterError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
var SpeechProviderError = class extends SpeechError {
|
|
25
|
+
status;
|
|
26
|
+
responseBody;
|
|
27
|
+
/** Parsed `Retry-After` delay in milliseconds, when the provider sent one. */
|
|
28
|
+
retryAfterMs;
|
|
29
|
+
constructor(adapter, message, options = {}) {
|
|
30
|
+
super(message, "SPEECH_PROVIDER_ERROR", adapter);
|
|
31
|
+
this.name = "SpeechProviderError";
|
|
32
|
+
this.status = options.status;
|
|
33
|
+
this.responseBody = options.responseBody;
|
|
34
|
+
this.retryAfterMs = options.retryAfterMs;
|
|
35
|
+
if (options.cause !== void 0) this.cause = options.cause;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { SpeechProviderError as i, SpeechConfigurationError as n, SpeechError as r, InvalidSpeechAdapterError as t };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=errors-CQBX3OPS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-CQBX3OPS.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"],"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"}
|
|
@@ -1,41 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var SpeechError = class extends Error {
|
|
3
|
-
code;
|
|
4
|
-
adapter;
|
|
5
|
-
constructor(message, code, adapter) {
|
|
6
|
-
super(message);
|
|
7
|
-
this.code = code;
|
|
8
|
-
this.adapter = adapter;
|
|
9
|
-
this.name = "SpeechError";
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
var SpeechConfigurationError = class extends SpeechError {
|
|
13
|
-
constructor(message, adapter) {
|
|
14
|
-
super(message, "SPEECH_CONFIGURATION_ERROR", adapter);
|
|
15
|
-
this.name = "SpeechConfigurationError";
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
var InvalidSpeechAdapterError = class extends SpeechError {
|
|
19
|
-
constructor(type, kind) {
|
|
20
|
-
super(`Invalid ${kind} speech adapter type: ${type}`, "INVALID_ADAPTER");
|
|
21
|
-
this.name = "InvalidSpeechAdapterError";
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
var SpeechProviderError = class extends SpeechError {
|
|
25
|
-
status;
|
|
26
|
-
responseBody;
|
|
27
|
-
/** Parsed `Retry-After` delay in milliseconds, when the provider sent one. */
|
|
28
|
-
retryAfterMs;
|
|
29
|
-
constructor(adapter, message, options = {}) {
|
|
30
|
-
super(message, "SPEECH_PROVIDER_ERROR", adapter);
|
|
31
|
-
this.name = "SpeechProviderError";
|
|
32
|
-
this.status = options.status;
|
|
33
|
-
this.responseBody = options.responseBody;
|
|
34
|
-
this.retryAfterMs = options.retryAfterMs;
|
|
35
|
-
if (options.cause !== void 0) this.cause = options.cause;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
//#endregion
|
|
1
|
+
import { n as SpeechConfigurationError } from "./errors-CQBX3OPS.js";
|
|
39
2
|
//#region src/shared/audio.ts
|
|
40
3
|
/**
|
|
41
4
|
* Audio input normalisation shared by speech adapters: accepts the input
|
|
@@ -338,6 +301,6 @@ function registeredOptionalTranscribers() {
|
|
|
338
301
|
return [...registered.keys()];
|
|
339
302
|
}
|
|
340
303
|
//#endregion
|
|
341
|
-
export {
|
|
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 };
|
|
342
305
|
|
|
343
|
-
//# sourceMappingURL=registry-
|
|
306
|
+
//# sourceMappingURL=registry-C_5BakmM.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { n as SpeechConfigurationError } from "./errors-CQBX3OPS.js";
|
|
2
|
+
//#region src/shared/websocket.ts
|
|
3
|
+
/**
|
|
4
|
+
* Minimal WebSocket surface used by streaming adapters. Browser, Node 22+
|
|
5
|
+
* (`globalThis.WebSocket`), Deno, Bun, and the `ws` package all satisfy it.
|
|
6
|
+
*/
|
|
7
|
+
/** True in browsers and web workers, where API keys must never be used. */
|
|
8
|
+
function isBrowserRuntime() {
|
|
9
|
+
const scope = globalThis;
|
|
10
|
+
if (scope.process?.versions?.node) return false;
|
|
11
|
+
return typeof scope.document !== "undefined" || typeof scope.WorkerGlobalScope !== "undefined";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a socket factory from explicit options, falling back to
|
|
15
|
+
* `globalThis.WebSocket`. Headers are only passed when present, so standard
|
|
16
|
+
* constructors that cannot send them are never handed an options object.
|
|
17
|
+
*/
|
|
18
|
+
function resolveWebSocketFactory(adapter, options) {
|
|
19
|
+
if (options.createWebSocket) return options.createWebSocket;
|
|
20
|
+
const Ctor = options.WebSocket ?? globalThis.WebSocket;
|
|
21
|
+
if (!Ctor) throw new SpeechConfigurationError("No WebSocket implementation available; pass `WebSocket` or `createWebSocket`", adapter);
|
|
22
|
+
return (url, init) => Object.keys(init.headers).length > 0 ? new Ctor(url, {
|
|
23
|
+
protocols: init.protocols,
|
|
24
|
+
headers: init.headers
|
|
25
|
+
}) : new Ctor(url, init.protocols);
|
|
26
|
+
}
|
|
27
|
+
/** Converts `http(s)://` to `ws(s)://`; leaves `ws(s)://` unchanged. */
|
|
28
|
+
function toWebSocketUrl(url, adapter) {
|
|
29
|
+
const parsed = new URL(url);
|
|
30
|
+
if (parsed.protocol === "http:") parsed.protocol = "ws:";
|
|
31
|
+
else if (parsed.protocol === "https:") parsed.protocol = "wss:";
|
|
32
|
+
else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") throw new SpeechConfigurationError(`Unsupported streaming URL protocol: ${parsed.protocol}`, adapter);
|
|
33
|
+
return parsed.toString();
|
|
34
|
+
}
|
|
35
|
+
/** Converts a message payload to text; returns `undefined` for unsupported data. */
|
|
36
|
+
function messageDataToText(data) {
|
|
37
|
+
if (typeof data === "string") return data;
|
|
38
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
39
|
+
if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { toWebSocketUrl as i, messageDataToText as n, resolveWebSocketFactory as r, isBrowserRuntime as t };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=websocket-CR9uXAuS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket-CR9uXAuS.js","names":[],"sources":["../../src/shared/websocket.ts"],"sourcesContent":["/**\n * Minimal WebSocket surface used by streaming adapters. Browser, Node 22+\n * (`globalThis.WebSocket`), Deno, Bun, and the `ws` package all satisfy it.\n */\n\nimport { SpeechConfigurationError } from './errors.js';\n\n/** `WebSocket.OPEN`. */\nexport const WEBSOCKET_OPEN = 1;\n\nexport interface SpeechWebSocketMessageEvent {\n data: unknown;\n}\n\nexport interface SpeechWebSocketCloseEvent {\n code?: number;\n reason?: string;\n}\n\n/** The subset of the WHATWG `WebSocket` interface the adapters use. */\nexport interface SpeechWebSocket {\n readonly readyState: number;\n readonly bufferedAmount: number;\n binaryType?: string;\n send(data: string): void;\n close(code?: number, reason?: string): void;\n addEventListener(type: 'open', listener: (event: unknown) => void): void;\n addEventListener(\n type: 'message',\n listener: (event: SpeechWebSocketMessageEvent) => void,\n ): void;\n addEventListener(type: 'error', listener: (event: unknown) => void): void;\n addEventListener(\n type: 'close',\n listener: (event: SpeechWebSocketCloseEvent) => void,\n ): void;\n}\n\n/** Handshake details passed to a {@link SpeechWebSocketFactory}. */\nexport interface SpeechWebSocketInit {\n protocols: string[];\n /** Handshake headers; empty in browsers. */\n headers: Record<string, string>;\n}\n\nexport type SpeechWebSocketFactory = (\n url: string,\n init: SpeechWebSocketInit,\n) => SpeechWebSocket;\n\n/**\n * A WebSocket constructor. Standard constructors accept `protocols`; Node's\n * built-in WebSocket also accepts `{ protocols, headers }`.\n */\nexport type SpeechWebSocketConstructor = new (\n url: string,\n protocols?:\n | string\n | string[]\n | { protocols?: string[]; headers?: Record<string, string> },\n) => SpeechWebSocket;\n\n/** True in browsers and web workers, where API keys must never be used. */\nexport function isBrowserRuntime(): boolean {\n const scope = globalThis as {\n document?: unknown;\n WorkerGlobalScope?: unknown;\n process?: { versions?: { node?: string } };\n };\n if (scope.process?.versions?.node) {\n return false;\n }\n return (\n typeof scope.document !== 'undefined' ||\n typeof scope.WorkerGlobalScope !== 'undefined'\n );\n}\n\n/**\n * Resolves a socket factory from explicit options, falling back to\n * `globalThis.WebSocket`. Headers are only passed when present, so standard\n * constructors that cannot send them are never handed an options object.\n */\nexport function resolveWebSocketFactory(\n adapter: string,\n options: {\n createWebSocket?: SpeechWebSocketFactory;\n WebSocket?: SpeechWebSocketConstructor;\n },\n): SpeechWebSocketFactory {\n if (options.createWebSocket) {\n return options.createWebSocket;\n }\n\n const Ctor =\n options.WebSocket ??\n (globalThis as { WebSocket?: SpeechWebSocketConstructor }).WebSocket;\n if (!Ctor) {\n throw new SpeechConfigurationError(\n 'No WebSocket implementation available; pass `WebSocket` or `createWebSocket`',\n adapter,\n );\n }\n\n return (url, init) =>\n Object.keys(init.headers).length > 0\n ? new Ctor(url, { protocols: init.protocols, headers: init.headers })\n : new Ctor(url, init.protocols);\n}\n\n/** Converts `http(s)://` to `ws(s)://`; leaves `ws(s)://` unchanged. */\nexport function toWebSocketUrl(url: string, adapter: string): string {\n const parsed = new URL(url);\n if (parsed.protocol === 'http:') {\n parsed.protocol = 'ws:';\n } else if (parsed.protocol === 'https:') {\n parsed.protocol = 'wss:';\n } else if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {\n throw new SpeechConfigurationError(\n `Unsupported streaming URL protocol: ${parsed.protocol}`,\n adapter,\n );\n }\n return parsed.toString();\n}\n\n/** Converts a message payload to text; returns `undefined` for unsupported data. */\nexport function messageDataToText(data: unknown): string | undefined {\n if (typeof data === 'string') {\n return data;\n }\n if (data instanceof ArrayBuffer) {\n return new TextDecoder().decode(data);\n }\n if (ArrayBuffer.isView(data)) {\n return new TextDecoder().decode(data);\n }\n return undefined;\n}\n"],"mappings":";;;;;;;AA+DA,SAAgB,mBAA4B;CAC1C,MAAM,QAAQ;CAKd,IAAI,MAAM,SAAS,UAAU,MAC3B,OAAO;CAET,OACE,OAAO,MAAM,aAAa,eAC1B,OAAO,MAAM,sBAAsB;AAEvC;;;;;;AAOA,SAAgB,wBACd,SACA,SAIwB;CACxB,IAAI,QAAQ,iBACV,OAAO,QAAQ;CAGjB,MAAM,OACJ,QAAQ,aACP,WAA0D;CAC7D,IAAI,CAAC,MACH,MAAM,IAAI,yBACR,gFACA,OACF;CAGF,QAAQ,KAAK,SACX,OAAO,KAAK,KAAK,OAAO,CAAC,CAAC,SAAS,IAC/B,IAAI,KAAK,KAAK;EAAE,WAAW,KAAK;EAAW,SAAS,KAAK;CAAQ,CAAC,IAClE,IAAI,KAAK,KAAK,KAAK,SAAS;AACpC;;AAGA,SAAgB,eAAe,KAAa,SAAyB;CACnE,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,IAAI,OAAO,aAAa,SACtB,OAAO,WAAW;MACb,IAAI,OAAO,aAAa,UAC7B,OAAO,WAAW;MACb,IAAI,OAAO,aAAa,SAAS,OAAO,aAAa,QAC1D,MAAM,IAAI,yBACR,uCAAuC,OAAO,YAC9C,OACF;CAEF,OAAO,OAAO,SAAS;AACzB;;AAGA,SAAgB,kBAAkB,MAAmC;CACnE,IAAI,OAAO,SAAS,UAClB,OAAO;CAET,IAAI,gBAAgB,aAClB,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;CAEtC,IAAI,YAAY,OAAO,IAAI,GACzB,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;AAGxC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { OpenAIWebRTCVoiceOptions, VoiceSession } from './types.js';
|
|
2
|
+
/** Browser WebRTC transport. No credentials, Node dependencies or globals are read at import. */
|
|
3
|
+
export declare function createOpenAIWebRTCVoiceSession(options: OpenAIWebRTCVoiceOptions): VoiceSession;
|
|
4
|
+
//# sourceMappingURL=openai-webrtc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-webrtc.d.ts","sourceRoot":"","sources":["../../src/conversation/openai-webrtc.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,wBAAwB,EACxB,YAAY,EAIb,MAAM,YAAY,CAAC;AAIpB,iGAAiG;AACjG,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,wBAAwB,GAChC,YAAY,CAmbd"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Additive speech-to-speech conversation contract; independent of streaming STT. */
|
|
2
|
+
export type VoiceSessionState = 'idle' | 'connecting' | 'connected' | 'closed' | 'failed';
|
|
3
|
+
export interface VoiceTranscript {
|
|
4
|
+
itemId: string;
|
|
5
|
+
role: 'user' | 'assistant';
|
|
6
|
+
text: string;
|
|
7
|
+
final: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface VoiceSessionEvents {
|
|
10
|
+
state: VoiceSessionState;
|
|
11
|
+
transcript: VoiceTranscript;
|
|
12
|
+
/** Actual remote playback, rather than model response generation. */
|
|
13
|
+
speaking: boolean;
|
|
14
|
+
inputSpeech: boolean;
|
|
15
|
+
response: {
|
|
16
|
+
active: boolean;
|
|
17
|
+
responseId?: string;
|
|
18
|
+
};
|
|
19
|
+
usage: Record<string, unknown>;
|
|
20
|
+
toolCall: {
|
|
21
|
+
callId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
arguments: string;
|
|
24
|
+
};
|
|
25
|
+
/** Recoverable protocol/playback errors also use this event. */
|
|
26
|
+
error: Error;
|
|
27
|
+
}
|
|
28
|
+
export interface VoiceSession {
|
|
29
|
+
readonly state: VoiceSessionState;
|
|
30
|
+
/** Coalesced while connecting; terminal sessions cannot be reconnected. */
|
|
31
|
+
connect(): Promise<void>;
|
|
32
|
+
sendText(text: string): string;
|
|
33
|
+
/** Commit buffered microphone input in manual turn mode, then optionally generate a reply. */
|
|
34
|
+
commitInput(respond?: boolean): void;
|
|
35
|
+
/** Request a response, e.g. an opening introduction. */
|
|
36
|
+
respond(instructions?: string): void;
|
|
37
|
+
/** Cancels active generation and clears unplayed WebRTC audio. */
|
|
38
|
+
interrupt(): void;
|
|
39
|
+
setMicMuted(muted: boolean): void;
|
|
40
|
+
setOutputMuted(muted: boolean): void;
|
|
41
|
+
/** Submit a tool output without generating a reply. After every tool in the completed response resolves, the host calls respond() once. */
|
|
42
|
+
submitToolResult(callId: string, result: unknown): void;
|
|
43
|
+
/** Idempotently releases tracks, peer connection, playback and listeners. */
|
|
44
|
+
close(): void;
|
|
45
|
+
on<E extends keyof VoiceSessionEvents>(event: E, listener: (value: VoiceSessionEvents[E]) => void): () => void;
|
|
46
|
+
}
|
|
47
|
+
export interface OpenAIWebRTCVoiceOptions {
|
|
48
|
+
/** Host-authenticated negotiation: post the offer to your server, return its answer. */
|
|
49
|
+
negotiate(offer: string, signal: AbortSignal): Promise<string>;
|
|
50
|
+
/** Called during connect. Ownership transfers to the session, including late resolution. */
|
|
51
|
+
getMicrophone(): Promise<MediaStream>;
|
|
52
|
+
audio?: HTMLAudioElement;
|
|
53
|
+
createPeerConnection?: () => RTCPeerConnection;
|
|
54
|
+
connectTimeoutMs?: number;
|
|
55
|
+
/** Starts muted to let the host introduce its character before listening. Default false. */
|
|
56
|
+
micMuted?: boolean;
|
|
57
|
+
outputMuted?: boolean;
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/conversation/types.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,YAAY,GACZ,WAAW,GACX,QAAQ,GACR,QAAQ,CAAC;AAEb,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,eAAe,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,gEAAgE;IAChE,KAAK,EAAE,KAAK,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,2EAA2E;IAC3E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,8FAA8F;IAC9F,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,wDAAwD;IACxD,OAAO,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,kEAAkE;IAClE,SAAS,IAAI,IAAI,CAAC;IAClB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,2IAA2I;IAC3I,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACxD,6EAA6E;IAC7E,KAAK,IAAI,IAAI,CAAC;IACd,EAAE,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACnC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAAK,IAAI,GAC/C,MAAM,IAAI,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,wFAAwF;IACxF,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/D,4FAA4F;IAC5F,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,iBAAiB,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SpeechProviderError } from './shared/errors.js';
|
|
2
|
+
import { StreamingTurnDetection } from './shared/streaming-types.js';
|
|
3
|
+
import { SpeechFetch } from './shared/types.js';
|
|
4
|
+
export interface OpenAIVoiceCallOptions {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Fixed by the host, never copied from untrusted browser JSON. */
|
|
7
|
+
model: string;
|
|
8
|
+
voice?: string;
|
|
9
|
+
instructions?: string;
|
|
10
|
+
transcriptionModel?: string;
|
|
11
|
+
turnDetection?: StreamingTurnDetection;
|
|
12
|
+
maxOutputTokens?: number;
|
|
13
|
+
tools?: Array<{
|
|
14
|
+
type: 'function';
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
parameters: Record<string, unknown>;
|
|
18
|
+
}>;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
fetch?: SpeechFetch;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface OpenAIVoiceCall {
|
|
25
|
+
answer: string;
|
|
26
|
+
callId: string;
|
|
27
|
+
}
|
|
28
|
+
/** Build the GA speech-to-speech session config. Transcription is an optional side channel. */
|
|
29
|
+
export declare function openAIConversationConfig(options: Pick<OpenAIVoiceCallOptions, 'model' | 'voice' | 'instructions' | 'transcriptionModel' | 'turnDetection' | 'maxOutputTokens' | 'tools'>): Record<string, unknown>;
|
|
30
|
+
/** No retries: retrying call creation can create duplicate billable sessions. */
|
|
31
|
+
export declare function createOpenAIVoiceCall(offer: string, options: OpenAIVoiceCallOptions): Promise<OpenAIVoiceCall>;
|
|
32
|
+
export type OpenAIVoiceCallControlOptions = Pick<OpenAIVoiceCallOptions, 'apiKey' | 'baseUrl' | 'fetch' | 'signal' | 'timeoutMs'>;
|
|
33
|
+
/** Hosts should call this on close, idle/hard timeout and failed client setup. */
|
|
34
|
+
export declare function hangupOpenAIVoiceCall(callId: string, options: OpenAIVoiceCallControlOptions): Promise<void>;
|
|
35
|
+
/** Failed setup whose cleanup also failed. The host must reserve admission and retry termination. */
|
|
36
|
+
export declare class VoiceCallSetupError extends SpeechProviderError {
|
|
37
|
+
readonly callId: string;
|
|
38
|
+
constructor(callId: string);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=conversation-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-server.d.ts","sourceRoot":"","sources":["../src/conversation-server.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,OAAO,EAEL,mBAAmB,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+FAA+F;AAC/F,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,IAAI,CACX,sBAAsB,EACpB,OAAO,GACP,OAAO,GACP,cAAc,GACd,oBAAoB,GACpB,eAAe,GACf,iBAAiB,GACjB,OAAO,CACV,GACA,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0DzB;AAED,iFAAiF;AACjF,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,eAAe,CAAC,CA0C1B;AAED,MAAM,MAAM,6BAA6B,GAAG,IAAI,CAC9C,sBAAsB,EACtB,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CACxD,CAAC;AAEF,kFAAkF;AAClF,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAKf;AAuFD,qGAAqG;AACrG,qBAAa,mBAAoB,SAAQ,mBAAmB;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAMpC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { i as SpeechProviderError, n as SpeechConfigurationError } from "./chunks/errors-CQBX3OPS.js";
|
|
2
|
+
import { t as isBrowserRuntime } from "./chunks/websocket-CR9uXAuS.js";
|
|
3
|
+
//#region src/conversation-server.ts
|
|
4
|
+
/** Server-only OpenAI conversational call lifecycle. Hosts own authorization and admission policy. */
|
|
5
|
+
/** Build the GA speech-to-speech session config. Transcription is an optional side channel. */
|
|
6
|
+
function openAIConversationConfig(options) {
|
|
7
|
+
if (!options.model?.trim()) throw new SpeechConfigurationError("Conversation model is required");
|
|
8
|
+
const detection = options.turnDetection ?? {
|
|
9
|
+
type: "semantic_vad",
|
|
10
|
+
eagerness: "auto"
|
|
11
|
+
};
|
|
12
|
+
let turnDetection;
|
|
13
|
+
switch (detection.type) {
|
|
14
|
+
case "manual":
|
|
15
|
+
turnDetection = null;
|
|
16
|
+
break;
|
|
17
|
+
case "semantic_vad":
|
|
18
|
+
turnDetection = {
|
|
19
|
+
type: "semantic_vad",
|
|
20
|
+
eagerness: detection.eagerness ?? "auto",
|
|
21
|
+
create_response: true,
|
|
22
|
+
interrupt_response: true
|
|
23
|
+
};
|
|
24
|
+
break;
|
|
25
|
+
case "server_vad":
|
|
26
|
+
turnDetection = {
|
|
27
|
+
type: "server_vad",
|
|
28
|
+
...detection.threshold === void 0 ? {} : { threshold: detection.threshold },
|
|
29
|
+
...detection.prefixPaddingMs === void 0 ? {} : { prefix_padding_ms: detection.prefixPaddingMs },
|
|
30
|
+
...detection.silenceDurationMs === void 0 ? {} : { silence_duration_ms: detection.silenceDurationMs },
|
|
31
|
+
create_response: true,
|
|
32
|
+
interrupt_response: true
|
|
33
|
+
};
|
|
34
|
+
break;
|
|
35
|
+
default: throw new SpeechConfigurationError("Unknown conversation turn detection");
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
type: "realtime",
|
|
39
|
+
model: options.model.trim(),
|
|
40
|
+
output_modalities: ["audio"],
|
|
41
|
+
...options.instructions ? { instructions: options.instructions } : {},
|
|
42
|
+
audio: {
|
|
43
|
+
input: {
|
|
44
|
+
turn_detection: turnDetection,
|
|
45
|
+
...options.transcriptionModel ? { transcription: { model: options.transcriptionModel } } : {}
|
|
46
|
+
},
|
|
47
|
+
output: { voice: options.voice ?? "marin" }
|
|
48
|
+
},
|
|
49
|
+
...options.maxOutputTokens === void 0 ? {} : { max_output_tokens: options.maxOutputTokens },
|
|
50
|
+
...options.tools ? { tools: options.tools } : {}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** No retries: retrying call creation can create duplicate billable sessions. */
|
|
54
|
+
async function createOpenAIVoiceCall(offer, options) {
|
|
55
|
+
assertServer(options);
|
|
56
|
+
if (!validSdp(offer)) throw new SpeechConfigurationError("Expected a bounded SDP offer");
|
|
57
|
+
const body = new FormData();
|
|
58
|
+
body.set("sdp", offer);
|
|
59
|
+
body.set("session", JSON.stringify(openAIConversationConfig(options)));
|
|
60
|
+
let callId;
|
|
61
|
+
try {
|
|
62
|
+
const answer = await (await request(callBase(options.baseUrl), { body }, options, (headers) => {
|
|
63
|
+
const value = headers.get("location")?.split("/").pop();
|
|
64
|
+
if (!value || !/^rtc_[A-Za-z0-9_-]+$/.test(value)) throw new SpeechProviderError("openai-conversation", "Call response did not contain a WebRTC call id");
|
|
65
|
+
callId = value;
|
|
66
|
+
})).text();
|
|
67
|
+
if (!validSdp(answer)) throw new SpeechProviderError("openai-conversation", "Call response did not contain a valid SDP answer");
|
|
68
|
+
options.signal?.throwIfAborted();
|
|
69
|
+
return {
|
|
70
|
+
answer,
|
|
71
|
+
callId
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (callId) try {
|
|
75
|
+
await hangupOpenAIVoiceCall(callId, {
|
|
76
|
+
...options,
|
|
77
|
+
signal: void 0
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
throw new VoiceCallSetupError(callId);
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Hosts should call this on close, idle/hard timeout and failed client setup. */
|
|
86
|
+
async function hangupOpenAIVoiceCall(callId, options) {
|
|
87
|
+
assertServer(options);
|
|
88
|
+
if (!/^rtc_[A-Za-z0-9_-]+$/.test(callId)) throw new SpeechConfigurationError("Invalid WebRTC call id");
|
|
89
|
+
await request(`${callBase(options.baseUrl)}/${callId}/hangup`, {}, options);
|
|
90
|
+
}
|
|
91
|
+
function assertServer(options) {
|
|
92
|
+
if (isBrowserRuntime()) throw new SpeechConfigurationError("Voice call management is server-only; never expose a provider key");
|
|
93
|
+
if (!options.apiKey?.trim()) throw new SpeechConfigurationError("Voice call management requires a server API key");
|
|
94
|
+
const timeout = options.timeoutMs ?? 15e3;
|
|
95
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new SpeechConfigurationError("timeoutMs must be positive");
|
|
96
|
+
}
|
|
97
|
+
function callBase(baseUrl = "https://api.openai.com/v1") {
|
|
98
|
+
const url = new URL(baseUrl);
|
|
99
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new SpeechConfigurationError("Invalid voice call API base URL");
|
|
100
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}${/\/v\d+$/.test(url.pathname.replace(/\/$/, "")) ? "" : "/v1"}/realtime/calls`;
|
|
101
|
+
return url.toString();
|
|
102
|
+
}
|
|
103
|
+
function validSdp(value) {
|
|
104
|
+
return typeof value === "string" && value.length <= 65536 && value.startsWith("v=0") && value.includes("m=audio");
|
|
105
|
+
}
|
|
106
|
+
async function request(url, init, options, onHeaders) {
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 15e3);
|
|
109
|
+
const signal = AbortSignal.any([controller.signal, ...options.signal ? [options.signal] : []]);
|
|
110
|
+
try {
|
|
111
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
112
|
+
...init,
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { authorization: `Bearer ${options.apiKey}` },
|
|
115
|
+
signal
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) throw new SpeechProviderError("openai-conversation", "OpenAI voice call request failed", { status: response.status });
|
|
118
|
+
onHeaders?.(response.headers);
|
|
119
|
+
const body = await readBody(response, signal);
|
|
120
|
+
return new Response(response.status === 204 ? null : body, {
|
|
121
|
+
status: response.status,
|
|
122
|
+
headers: response.headers
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof SpeechProviderError) throw error;
|
|
126
|
+
throw new SpeechProviderError("openai-conversation", signal.aborted ? "OpenAI voice call request cancelled or timed out" : "OpenAI voice call transport failed");
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Failed setup whose cleanup also failed. The host must reserve admission and retry termination. */
|
|
132
|
+
var VoiceCallSetupError = class extends SpeechProviderError {
|
|
133
|
+
callId;
|
|
134
|
+
constructor(callId) {
|
|
135
|
+
super("openai-conversation", "Voice setup failed and termination needs retry");
|
|
136
|
+
this.callId = callId;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
async function readBody(response, signal) {
|
|
140
|
+
const reader = response.body?.getReader();
|
|
141
|
+
if (!reader) return "";
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let size = 0;
|
|
144
|
+
const cancel = () => {
|
|
145
|
+
reader.cancel().catch(() => void 0);
|
|
146
|
+
};
|
|
147
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
148
|
+
try {
|
|
149
|
+
while (true) {
|
|
150
|
+
signal.throwIfAborted();
|
|
151
|
+
const { done, value } = await reader.read();
|
|
152
|
+
signal.throwIfAborted();
|
|
153
|
+
if (done) break;
|
|
154
|
+
size += value.byteLength;
|
|
155
|
+
if (size > 65536) throw new SpeechProviderError("openai-conversation", "Voice call response exceeded its size limit");
|
|
156
|
+
chunks.push(value);
|
|
157
|
+
}
|
|
158
|
+
const data = new Uint8Array(size);
|
|
159
|
+
let offset = 0;
|
|
160
|
+
for (const chunk of chunks) {
|
|
161
|
+
data.set(chunk, offset);
|
|
162
|
+
offset += chunk.byteLength;
|
|
163
|
+
}
|
|
164
|
+
return new TextDecoder().decode(data);
|
|
165
|
+
} finally {
|
|
166
|
+
signal.removeEventListener("abort", cancel);
|
|
167
|
+
await reader.cancel().catch(() => void 0);
|
|
168
|
+
reader.releaseLock();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { VoiceCallSetupError, createOpenAIVoiceCall, hangupOpenAIVoiceCall, openAIConversationConfig };
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=conversation-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-server.js","names":[],"sources":["../src/conversation-server.ts"],"sourcesContent":["/** Server-only OpenAI conversational call lifecycle. Hosts own authorization and admission policy. */\nimport {\n SpeechConfigurationError,\n SpeechProviderError,\n} from './shared/errors.js';\nimport type { StreamingTurnDetection } from './shared/streaming-types.js';\nimport type { SpeechFetch } from './shared/types.js';\nimport { isBrowserRuntime } from './shared/websocket.js';\n\nexport interface OpenAIVoiceCallOptions {\n apiKey: string;\n /** Fixed by the host, never copied from untrusted browser JSON. */\n model: string;\n voice?: string;\n instructions?: string;\n transcriptionModel?: string;\n turnDetection?: StreamingTurnDetection;\n maxOutputTokens?: number;\n tools?: Array<{\n type: 'function';\n name: string;\n description?: string;\n parameters: Record<string, unknown>;\n }>;\n baseUrl?: string;\n fetch?: SpeechFetch;\n signal?: AbortSignal;\n timeoutMs?: number;\n}\n\nexport interface OpenAIVoiceCall {\n answer: string;\n callId: string;\n}\n\n/** Build the GA speech-to-speech session config. Transcription is an optional side channel. */\nexport function openAIConversationConfig(\n options: Pick<\n OpenAIVoiceCallOptions,\n | 'model'\n | 'voice'\n | 'instructions'\n | 'transcriptionModel'\n | 'turnDetection'\n | 'maxOutputTokens'\n | 'tools'\n >,\n): Record<string, unknown> {\n if (!options.model?.trim())\n throw new SpeechConfigurationError('Conversation model is required');\n const detection = options.turnDetection ?? {\n type: 'semantic_vad',\n eagerness: 'auto',\n };\n let turnDetection: Record<string, unknown> | null;\n switch (detection.type) {\n case 'manual':\n turnDetection = null;\n break;\n case 'semantic_vad':\n turnDetection = {\n type: 'semantic_vad',\n eagerness: detection.eagerness ?? 'auto',\n create_response: true,\n interrupt_response: true,\n };\n break;\n case 'server_vad':\n turnDetection = {\n type: 'server_vad',\n ...(detection.threshold === undefined\n ? {}\n : { threshold: detection.threshold }),\n ...(detection.prefixPaddingMs === undefined\n ? {}\n : { prefix_padding_ms: detection.prefixPaddingMs }),\n ...(detection.silenceDurationMs === undefined\n ? {}\n : { silence_duration_ms: detection.silenceDurationMs }),\n create_response: true,\n interrupt_response: true,\n };\n break;\n default:\n throw new SpeechConfigurationError('Unknown conversation turn detection');\n }\n return {\n type: 'realtime',\n model: options.model.trim(),\n output_modalities: ['audio'],\n ...(options.instructions ? { instructions: options.instructions } : {}),\n audio: {\n input: {\n turn_detection: turnDetection,\n ...(options.transcriptionModel\n ? { transcription: { model: options.transcriptionModel } }\n : {}),\n },\n output: { voice: options.voice ?? 'marin' },\n },\n ...(options.maxOutputTokens === undefined\n ? {}\n : { max_output_tokens: options.maxOutputTokens }),\n ...(options.tools ? { tools: options.tools } : {}),\n };\n}\n\n/** No retries: retrying call creation can create duplicate billable sessions. */\nexport async function createOpenAIVoiceCall(\n offer: string,\n options: OpenAIVoiceCallOptions,\n): Promise<OpenAIVoiceCall> {\n assertServer(options);\n if (!validSdp(offer))\n throw new SpeechConfigurationError('Expected a bounded SDP offer');\n const body = new FormData();\n body.set('sdp', offer);\n body.set('session', JSON.stringify(openAIConversationConfig(options)));\n let callId: string | undefined;\n try {\n const response = await request(\n callBase(options.baseUrl),\n { body },\n options,\n (headers) => {\n const value = headers.get('location')?.split('/').pop();\n if (!value || !/^rtc_[A-Za-z0-9_-]+$/.test(value))\n throw new SpeechProviderError(\n 'openai-conversation',\n 'Call response did not contain a WebRTC call id',\n );\n // Capture identity before consuming a response body that can fail or time out.\n callId = value;\n },\n );\n const answer = await response.text();\n if (!validSdp(answer))\n throw new SpeechProviderError(\n 'openai-conversation',\n 'Call response did not contain a valid SDP answer',\n );\n options.signal?.throwIfAborted();\n return { answer, callId: callId! };\n } catch (error) {\n if (callId) {\n try {\n await hangupOpenAIVoiceCall(callId, { ...options, signal: undefined });\n } catch {\n throw new VoiceCallSetupError(callId);\n }\n }\n throw error;\n }\n}\n\nexport type OpenAIVoiceCallControlOptions = Pick<\n OpenAIVoiceCallOptions,\n 'apiKey' | 'baseUrl' | 'fetch' | 'signal' | 'timeoutMs'\n>;\n\n/** Hosts should call this on close, idle/hard timeout and failed client setup. */\nexport async function hangupOpenAIVoiceCall(\n callId: string,\n options: OpenAIVoiceCallControlOptions,\n): Promise<void> {\n assertServer(options);\n if (!/^rtc_[A-Za-z0-9_-]+$/.test(callId))\n throw new SpeechConfigurationError('Invalid WebRTC call id');\n await request(`${callBase(options.baseUrl)}/${callId}/hangup`, {}, options);\n}\n\nfunction assertServer(options: OpenAIVoiceCallControlOptions): void {\n if (isBrowserRuntime())\n throw new SpeechConfigurationError(\n 'Voice call management is server-only; never expose a provider key',\n );\n if (!options.apiKey?.trim())\n throw new SpeechConfigurationError(\n 'Voice call management requires a server API key',\n );\n const timeout = options.timeoutMs ?? 15_000;\n if (!Number.isFinite(timeout) || timeout <= 0)\n throw new SpeechConfigurationError('timeoutMs must be positive');\n}\n\nfunction callBase(baseUrl = 'https://api.openai.com/v1'): string {\n const url = new URL(baseUrl);\n if (\n !['http:', 'https:'].includes(url.protocol) ||\n url.username ||\n url.password ||\n url.search ||\n url.hash\n )\n throw new SpeechConfigurationError('Invalid voice call API base URL');\n url.pathname = `${url.pathname.replace(/\\/$/, '')}${/\\/v\\d+$/.test(url.pathname.replace(/\\/$/, '')) ? '' : '/v1'}/realtime/calls`;\n return url.toString();\n}\n\nfunction validSdp(value: string): boolean {\n return (\n typeof value === 'string' &&\n value.length <= 65_536 &&\n value.startsWith('v=0') &&\n value.includes('m=audio')\n );\n}\n\nasync function request(\n url: string,\n init: RequestInit,\n options: OpenAIVoiceCallControlOptions,\n onHeaders?: (headers: Headers) => void,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(\n () => controller.abort(),\n options.timeoutMs ?? 15_000,\n );\n const signal = AbortSignal.any([\n controller.signal,\n ...(options.signal ? [options.signal] : []),\n ]);\n try {\n const response = await (options.fetch ?? fetch)(url, {\n ...init,\n method: 'POST',\n headers: { authorization: `Bearer ${options.apiKey}` },\n signal,\n });\n if (!response.ok)\n throw new SpeechProviderError(\n 'openai-conversation',\n 'OpenAI voice call request failed',\n { status: response.status },\n );\n onHeaders?.(response.headers);\n const body = await readBody(response, signal);\n return new Response(response.status === 204 ? null : body, {\n status: response.status,\n headers: response.headers,\n });\n } catch (error) {\n if (error instanceof SpeechProviderError) throw error;\n // Deliberately omit provider payloads and transport messages, which can contain credentials.\n throw new SpeechProviderError(\n 'openai-conversation',\n signal.aborted\n ? 'OpenAI voice call request cancelled or timed out'\n : 'OpenAI voice call transport failed',\n );\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Failed setup whose cleanup also failed. The host must reserve admission and retry termination. */\nexport class VoiceCallSetupError extends SpeechProviderError {\n constructor(readonly callId: string) {\n super(\n 'openai-conversation',\n 'Voice setup failed and termination needs retry',\n );\n }\n}\n\nasync function readBody(\n response: Response,\n signal: AbortSignal,\n): Promise<string> {\n const reader = response.body?.getReader();\n if (!reader) return '';\n const chunks: Uint8Array[] = [];\n let size = 0;\n const cancel = () => {\n void reader.cancel().catch(() => undefined);\n };\n signal.addEventListener('abort', cancel, { once: true });\n try {\n while (true) {\n signal.throwIfAborted();\n const { done, value } = await reader.read();\n signal.throwIfAborted();\n if (done) break;\n size += value.byteLength;\n if (size > 65_536)\n throw new SpeechProviderError(\n 'openai-conversation',\n 'Voice call response exceeded its size limit',\n );\n chunks.push(value);\n }\n const data = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n data.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return new TextDecoder().decode(data);\n } finally {\n signal.removeEventListener('abort', cancel);\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Browser-safe, optional bidirectional voice conversation entry. */
|
|
2
|
+
export { createOpenAIWebRTCVoiceSession } from './conversation/openai-webrtc.js';
|
|
3
|
+
export type { OpenAIWebRTCVoiceOptions, VoiceSession, VoiceSessionEvents, VoiceSessionState, VoiceTranscript, } from './conversation/types.js';
|
|
4
|
+
//# sourceMappingURL=conversation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../src/conversation.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,EAAE,8BAA8B,EAAE,MAAM,iCAAiC,CAAC;AACjF,YAAY,EACV,wBAAwB,EACxB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,GAChB,MAAM,yBAAyB,CAAC"}
|