@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.
@@ -0,0 +1,346 @@
1
+ import { i as SpeechProviderError, n as SpeechConfigurationError } from "./chunks/errors-CQBX3OPS.js";
2
+ //#region src/conversation/openai-webrtc.ts
3
+ var ADAPTER = "openai-conversation";
4
+ /** Browser WebRTC transport. No credentials, Node dependencies or globals are read at import. */
5
+ function createOpenAIWebRTCVoiceSession(options) {
6
+ let state = "idle";
7
+ let pending;
8
+ let peer;
9
+ let channel;
10
+ let microphone;
11
+ let audio;
12
+ let micMuted = options.micMuted ?? false;
13
+ let outputMuted = options.outputMuted ?? false;
14
+ let responseId;
15
+ let responseActive = false;
16
+ let speaking = false;
17
+ const abort = new AbortController();
18
+ const listeners = /* @__PURE__ */ new Map();
19
+ const turns = /* @__PURE__ */ new Map();
20
+ const disposers = [];
21
+ function emit(event, value) {
22
+ for (const listener of listeners.get(event) ?? []) listener(value);
23
+ }
24
+ function change(next) {
25
+ state = next;
26
+ emit("state", next);
27
+ }
28
+ function playback(value) {
29
+ if (speaking === value) return;
30
+ speaking = value;
31
+ emit("speaking", value);
32
+ }
33
+ function cleanup() {
34
+ abort.abort();
35
+ for (const dispose of disposers.splice(0)) dispose();
36
+ microphone?.getTracks().forEach((track) => {
37
+ track.stop();
38
+ });
39
+ microphone = void 0;
40
+ if (channel) {
41
+ channel.onopen = channel.onmessage = channel.onclose = channel.onerror = null;
42
+ channel.close();
43
+ }
44
+ if (peer) {
45
+ peer.ontrack = peer.onconnectionstatechange = null;
46
+ peer.close();
47
+ }
48
+ if (audio) {
49
+ audio.pause();
50
+ audio.srcObject = null;
51
+ }
52
+ playback(false);
53
+ responseActive = false;
54
+ turns.clear();
55
+ }
56
+ function fail(message) {
57
+ if (state === "closed" || state === "failed") return;
58
+ state = "failed";
59
+ cleanup();
60
+ emit("state", state);
61
+ emit("error", new SpeechProviderError(ADAPTER, message));
62
+ }
63
+ function send(event) {
64
+ if (state !== "connected" || channel?.readyState !== "open") throw new SpeechConfigurationError("Voice session is not connected", ADAPTER);
65
+ channel.send(JSON.stringify(event));
66
+ }
67
+ function transcript(itemId, role, text, final, delta = false) {
68
+ if (typeof itemId !== "string" || typeof text !== "string") return;
69
+ const prior = turns.get(itemId);
70
+ if (prior?.final || prior && prior.role !== role) return;
71
+ const turn = {
72
+ itemId,
73
+ role,
74
+ text: delta ? (prior?.text ?? "") + text : text,
75
+ final
76
+ };
77
+ turns.set(itemId, turn);
78
+ emit("transcript", turn);
79
+ }
80
+ function receive(data) {
81
+ let event;
82
+ try {
83
+ const parsed = typeof data === "string" ? JSON.parse(data) : data;
84
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
85
+ event = parsed;
86
+ } catch {
87
+ return;
88
+ }
89
+ switch (event.type) {
90
+ case "conversation.item.input_audio_transcription.delta":
91
+ transcript(event.item_id, "user", event.delta, false, true);
92
+ break;
93
+ case "conversation.item.input_audio_transcription.completed":
94
+ transcript(event.item_id, "user", event.transcript, true);
95
+ break;
96
+ case "conversation.item.input_audio_transcription.failed":
97
+ emit("error", new SpeechProviderError(ADAPTER, "Input transcription failed"));
98
+ break;
99
+ case "response.output_audio_transcript.delta":
100
+ transcript(event.item_id, "assistant", event.delta, false, true);
101
+ break;
102
+ case "response.output_audio_transcript.done":
103
+ transcript(event.item_id, "assistant", event.transcript, true);
104
+ break;
105
+ case "response.output_text.delta":
106
+ transcript(event.item_id, "assistant", event.delta, false, true);
107
+ break;
108
+ case "response.output_text.done":
109
+ transcript(event.item_id, "assistant", event.text, true);
110
+ break;
111
+ case "input_audio_buffer.speech_started":
112
+ emit("inputSpeech", true);
113
+ break;
114
+ case "input_audio_buffer.speech_stopped":
115
+ emit("inputSpeech", false);
116
+ break;
117
+ case "output_audio_buffer.started":
118
+ playback(!outputMuted);
119
+ break;
120
+ case "output_audio_buffer.stopped":
121
+ case "output_audio_buffer.cleared":
122
+ playback(false);
123
+ break;
124
+ case "response.created": {
125
+ const response = record(event.response);
126
+ responseId = typeof response.id === "string" ? response.id : void 0;
127
+ responseActive = true;
128
+ emit("response", {
129
+ active: true,
130
+ responseId
131
+ });
132
+ break;
133
+ }
134
+ case "response.done": {
135
+ const response = record(event.response);
136
+ responseActive = false;
137
+ emit("response", {
138
+ active: false,
139
+ responseId
140
+ });
141
+ if (response.usage && typeof response.usage === "object") emit("usage", record(response.usage));
142
+ if (response.status === "failed" || response.status === "incomplete") emit("error", new SpeechProviderError(ADAPTER, response.status === "incomplete" ? "Voice response incomplete" : "Voice response failed"));
143
+ break;
144
+ }
145
+ case "response.function_call_arguments.done":
146
+ if (typeof event.call_id === "string" && typeof event.name === "string" && typeof event.arguments === "string") emit("toolCall", {
147
+ callId: event.call_id,
148
+ name: event.name,
149
+ arguments: event.arguments
150
+ });
151
+ break;
152
+ case "error":
153
+ emit("error", new SpeechProviderError(ADAPTER, "Realtime provider rejected a conversation event"));
154
+ break;
155
+ }
156
+ }
157
+ const session = {
158
+ get state() {
159
+ return state;
160
+ },
161
+ connect() {
162
+ if (pending) return pending;
163
+ if (state === "connected") return Promise.resolve();
164
+ if (state !== "idle") return Promise.reject(new SpeechConfigurationError("Create a new voice session after close or failure", ADAPTER));
165
+ const timeoutMs = options.connectTimeoutMs ?? 2e4;
166
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return Promise.reject(new SpeechConfigurationError("connectTimeoutMs must be positive", ADAPTER));
167
+ change("connecting");
168
+ const signal = AbortSignal.any([abort.signal, ...options.signal ? [options.signal] : []]);
169
+ pending = (async () => {
170
+ const timeout = setTimeout(() => fail("Voice connection timed out"), timeoutMs);
171
+ const onAbort = () => {
172
+ if (state !== "closed" && state !== "failed") fail("Voice connection cancelled");
173
+ };
174
+ signal.addEventListener("abort", onAbort, { once: true });
175
+ disposers.push(() => signal.removeEventListener("abort", onAbort));
176
+ try {
177
+ signal.throwIfAborted();
178
+ const stream = await abortable(options.getMicrophone().then((stream) => {
179
+ if (signal.aborted) stream.getTracks().forEach((track) => {
180
+ track.stop();
181
+ });
182
+ return stream;
183
+ }), signal);
184
+ signal.throwIfAborted();
185
+ microphone = stream;
186
+ stream.getAudioTracks().forEach((track) => {
187
+ track.enabled = !micMuted;
188
+ const ended = () => fail("Microphone ended; allow access and start a new session");
189
+ track.addEventListener("ended", ended);
190
+ disposers.push(() => track.removeEventListener("ended", ended));
191
+ if (track.readyState === "ended") throw new SpeechConfigurationError("Microphone stream has ended", ADAPTER);
192
+ });
193
+ if (!stream.getAudioTracks().length) throw new SpeechConfigurationError("Microphone stream has no audio tracks", ADAPTER);
194
+ peer = options.createPeerConnection?.() ?? new RTCPeerConnection();
195
+ audio = options.audio ?? new Audio();
196
+ audio.autoplay = true;
197
+ audio.muted = outputMuted;
198
+ peer.ontrack = (event) => {
199
+ if (!audio || signal.aborted) return;
200
+ audio.srcObject = event.streams[0] ?? new MediaStream([event.track]);
201
+ audio.play().catch(() => emit("error", new SpeechProviderError(ADAPTER, "Audio playback needs a user gesture")));
202
+ };
203
+ peer.onconnectionstatechange = () => {
204
+ if (peer?.connectionState === "failed" || peer?.connectionState === "disconnected") fail("Voice connection lost; start a new session");
205
+ };
206
+ for (const track of stream.getAudioTracks()) peer.addTrack(track, stream);
207
+ channel = peer.createDataChannel("oai-events");
208
+ channel.onmessage = (event) => receive(event.data);
209
+ const opened = new Promise((resolve, reject) => {
210
+ const cancel = () => reject(new SpeechProviderError(ADAPTER, "Voice connection cancelled"));
211
+ signal.addEventListener("abort", cancel, { once: true });
212
+ disposers.push(() => signal.removeEventListener("abort", cancel));
213
+ if (channel) {
214
+ channel.onopen = () => {
215
+ signal.removeEventListener("abort", cancel);
216
+ resolve();
217
+ };
218
+ channel.onclose = () => {
219
+ fail("Voice channel closed");
220
+ reject(new SpeechProviderError(ADAPTER, "Voice channel closed"));
221
+ };
222
+ channel.onerror = () => {
223
+ fail("Voice channel failed");
224
+ reject(new SpeechProviderError(ADAPTER, "Voice channel failed"));
225
+ };
226
+ }
227
+ });
228
+ opened.catch(() => void 0);
229
+ const offer = await abortable(peer.createOffer(), signal);
230
+ await abortable(peer.setLocalDescription(offer), signal);
231
+ signal.throwIfAborted();
232
+ if (!offer.sdp) throw new SpeechConfigurationError("WebRTC did not produce an SDP offer", ADAPTER);
233
+ const answer = await abortable(options.negotiate(offer.sdp, signal), signal);
234
+ signal.throwIfAborted();
235
+ await abortable(peer.setRemoteDescription({
236
+ type: "answer",
237
+ sdp: answer
238
+ }), signal);
239
+ await opened;
240
+ signal.throwIfAborted();
241
+ change("connected");
242
+ } catch (error) {
243
+ fail("Unable to connect voice session");
244
+ throw error;
245
+ } finally {
246
+ clearTimeout(timeout);
247
+ }
248
+ })().finally(() => {
249
+ pending = void 0;
250
+ });
251
+ return pending;
252
+ },
253
+ sendText(text) {
254
+ const trimmed = text.trim();
255
+ if (!trimmed || trimmed.length > 16e3) throw new SpeechConfigurationError("Text must contain 1–16000 characters", ADAPTER);
256
+ const id = `hv_${crypto.randomUUID().replaceAll("-", "").slice(0, 24)}`;
257
+ send({
258
+ type: "conversation.item.create",
259
+ item: {
260
+ id,
261
+ type: "message",
262
+ role: "user",
263
+ content: [{
264
+ type: "input_text",
265
+ text: trimmed
266
+ }]
267
+ }
268
+ });
269
+ transcript(id, "user", trimmed, true);
270
+ session.respond();
271
+ return id;
272
+ },
273
+ commitInput(respond = true) {
274
+ send({ type: "input_audio_buffer.commit" });
275
+ if (respond) session.respond();
276
+ },
277
+ respond(instructions) {
278
+ send({
279
+ type: "response.create",
280
+ ...instructions ? { response: { instructions } } : {}
281
+ });
282
+ },
283
+ interrupt() {
284
+ if (state !== "connected") return;
285
+ if (responseActive) send({
286
+ type: "response.cancel",
287
+ ...responseId ? { response_id: responseId } : {}
288
+ });
289
+ send({ type: "output_audio_buffer.clear" });
290
+ playback(false);
291
+ },
292
+ setMicMuted(muted) {
293
+ micMuted = muted;
294
+ microphone?.getAudioTracks().forEach((track) => {
295
+ track.enabled = !muted;
296
+ });
297
+ if (muted && state === "connected") send({ type: "input_audio_buffer.clear" });
298
+ },
299
+ setOutputMuted(muted) {
300
+ outputMuted = muted;
301
+ if (audio) audio.muted = muted;
302
+ if (muted) playback(false);
303
+ },
304
+ submitToolResult(callId, result) {
305
+ send({
306
+ type: "conversation.item.create",
307
+ item: {
308
+ type: "function_call_output",
309
+ call_id: callId,
310
+ output: JSON.stringify(result)
311
+ }
312
+ });
313
+ },
314
+ close() {
315
+ if (state === "closed") return;
316
+ state = "closed";
317
+ cleanup();
318
+ emit("state", state);
319
+ listeners.clear();
320
+ },
321
+ on(event, listener) {
322
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
323
+ set.add(listener);
324
+ listeners.set(event, set);
325
+ return () => {
326
+ set.delete(listener);
327
+ };
328
+ }
329
+ };
330
+ return session;
331
+ }
332
+ function record(value) {
333
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
334
+ }
335
+ function abortable(promise, signal) {
336
+ if (signal.aborted) return Promise.reject(signal.reason);
337
+ return new Promise((resolve, reject) => {
338
+ const cancel = () => reject(signal.reason);
339
+ signal.addEventListener("abort", cancel, { once: true });
340
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", cancel));
341
+ });
342
+ }
343
+ //#endregion
344
+ export { createOpenAIWebRTCVoiceSession };
345
+
346
+ //# sourceMappingURL=conversation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation.js","names":[],"sources":["../src/conversation/openai-webrtc.ts"],"sourcesContent":["import {\n SpeechConfigurationError,\n SpeechProviderError,\n} from '../shared/errors.js';\nimport type {\n OpenAIWebRTCVoiceOptions,\n VoiceSession,\n VoiceSessionEvents,\n VoiceSessionState,\n VoiceTranscript,\n} from './types.js';\n\nconst ADAPTER = 'openai-conversation';\n\n/** Browser WebRTC transport. No credentials, Node dependencies or globals are read at import. */\nexport function createOpenAIWebRTCVoiceSession(\n options: OpenAIWebRTCVoiceOptions,\n): VoiceSession {\n let state: VoiceSessionState = 'idle';\n let pending: Promise<void> | undefined;\n let peer: RTCPeerConnection | undefined;\n let channel: RTCDataChannel | undefined;\n let microphone: MediaStream | undefined;\n let audio: HTMLAudioElement | undefined;\n let micMuted = options.micMuted ?? false;\n let outputMuted = options.outputMuted ?? false;\n let responseId: string | undefined;\n let responseActive = false;\n let speaking = false;\n const abort = new AbortController();\n const listeners = new Map<\n keyof VoiceSessionEvents,\n Set<(value: never) => void>\n >();\n const turns = new Map<string, VoiceTranscript>();\n const disposers: (() => void)[] = [];\n\n function emit<E extends keyof VoiceSessionEvents>(\n event: E,\n value: VoiceSessionEvents[E],\n ): void {\n for (const listener of listeners.get(event) ?? []) listener(value as never);\n }\n function change(next: VoiceSessionState): void {\n state = next;\n emit('state', next);\n }\n function playback(value: boolean): void {\n if (speaking === value) return;\n speaking = value;\n emit('speaking', value);\n }\n function cleanup(): void {\n abort.abort();\n for (const dispose of disposers.splice(0)) dispose();\n microphone?.getTracks().forEach((track) => {\n track.stop();\n });\n microphone = undefined;\n if (channel) {\n channel.onopen =\n channel.onmessage =\n channel.onclose =\n channel.onerror =\n null;\n channel.close();\n }\n if (peer) {\n peer.ontrack = peer.onconnectionstatechange = null;\n peer.close();\n }\n if (audio) {\n audio.pause();\n audio.srcObject = null;\n }\n playback(false);\n responseActive = false;\n turns.clear();\n }\n function fail(message: string): void {\n if (state === 'closed' || state === 'failed') return;\n state = 'failed';\n cleanup();\n emit('state', state);\n emit('error', new SpeechProviderError(ADAPTER, message));\n }\n function send(event: Record<string, unknown>): void {\n if (state !== 'connected' || channel?.readyState !== 'open')\n throw new SpeechConfigurationError(\n 'Voice session is not connected',\n ADAPTER,\n );\n channel.send(JSON.stringify(event));\n }\n function transcript(\n itemId: unknown,\n role: VoiceTranscript['role'],\n text: unknown,\n final: boolean,\n delta = false,\n ): void {\n if (typeof itemId !== 'string' || typeof text !== 'string') return;\n const prior = turns.get(itemId);\n // Late duplicates/deltas must not overwrite a final turn or its role.\n if (prior?.final || (prior && prior.role !== role)) return;\n const turn = {\n itemId,\n role,\n text: delta ? (prior?.text ?? '') + text : text,\n final,\n };\n turns.set(itemId, turn);\n emit('transcript', turn);\n }\n function receive(data: unknown): void {\n let event: Record<string, unknown>;\n try {\n const parsed: unknown =\n typeof data === 'string' ? JSON.parse(data) : data;\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))\n return;\n event = parsed as Record<string, unknown>;\n } catch {\n return;\n }\n switch (event.type) {\n case 'conversation.item.input_audio_transcription.delta':\n transcript(event.item_id, 'user', event.delta, false, true);\n break;\n case 'conversation.item.input_audio_transcription.completed':\n transcript(event.item_id, 'user', event.transcript, true);\n break;\n case 'conversation.item.input_audio_transcription.failed':\n emit(\n 'error',\n new SpeechProviderError(ADAPTER, 'Input transcription failed'),\n );\n break;\n case 'response.output_audio_transcript.delta':\n transcript(event.item_id, 'assistant', event.delta, false, true);\n break;\n case 'response.output_audio_transcript.done':\n transcript(event.item_id, 'assistant', event.transcript, true);\n break;\n case 'response.output_text.delta':\n transcript(event.item_id, 'assistant', event.delta, false, true);\n break;\n case 'response.output_text.done':\n transcript(event.item_id, 'assistant', event.text, true);\n break;\n case 'input_audio_buffer.speech_started':\n emit('inputSpeech', true);\n break;\n case 'input_audio_buffer.speech_stopped':\n emit('inputSpeech', false);\n break;\n case 'output_audio_buffer.started':\n playback(!outputMuted);\n break;\n case 'output_audio_buffer.stopped':\n case 'output_audio_buffer.cleared':\n playback(false);\n break;\n case 'response.created': {\n const response = record(event.response);\n responseId = typeof response.id === 'string' ? response.id : undefined;\n responseActive = true;\n emit('response', { active: true, responseId });\n break;\n }\n case 'response.done': {\n const response = record(event.response);\n responseActive = false;\n emit('response', { active: false, responseId });\n if (response.usage && typeof response.usage === 'object')\n emit('usage', record(response.usage));\n if (response.status === 'failed' || response.status === 'incomplete')\n emit(\n 'error',\n new SpeechProviderError(\n ADAPTER,\n response.status === 'incomplete'\n ? 'Voice response incomplete'\n : 'Voice response failed',\n ),\n );\n break;\n }\n case 'response.function_call_arguments.done':\n if (\n typeof event.call_id === 'string' &&\n typeof event.name === 'string' &&\n typeof event.arguments === 'string'\n )\n emit('toolCall', {\n callId: event.call_id,\n name: event.name,\n arguments: event.arguments,\n });\n break;\n case 'error':\n emit(\n 'error',\n new SpeechProviderError(\n ADAPTER,\n 'Realtime provider rejected a conversation event',\n ),\n );\n break;\n }\n }\n\n const session: VoiceSession = {\n get state() {\n return state;\n },\n connect() {\n if (pending) return pending;\n if (state === 'connected') return Promise.resolve();\n if (state !== 'idle')\n return Promise.reject(\n new SpeechConfigurationError(\n 'Create a new voice session after close or failure',\n ADAPTER,\n ),\n );\n const timeoutMs = options.connectTimeoutMs ?? 20_000;\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)\n return Promise.reject(\n new SpeechConfigurationError(\n 'connectTimeoutMs must be positive',\n ADAPTER,\n ),\n );\n change('connecting');\n const signal = AbortSignal.any([\n abort.signal,\n ...(options.signal ? [options.signal] : []),\n ]);\n pending = (async () => {\n const timeout = setTimeout(\n () => fail('Voice connection timed out'),\n timeoutMs,\n );\n const onAbort = () => {\n if (state !== 'closed' && state !== 'failed')\n fail('Voice connection cancelled');\n };\n signal.addEventListener('abort', onAbort, { once: true });\n disposers.push(() => signal.removeEventListener('abort', onAbort));\n try {\n signal.throwIfAborted();\n // Permission acquisition cannot be aborted; dispose even if it resolves after close.\n const acquisition = options.getMicrophone().then((stream) => {\n if (signal.aborted)\n stream.getTracks().forEach((track) => {\n track.stop();\n });\n return stream;\n });\n const stream = await abortable(acquisition, signal);\n signal.throwIfAborted();\n microphone = stream;\n stream.getAudioTracks().forEach((track) => {\n track.enabled = !micMuted;\n const ended = () =>\n fail('Microphone ended; allow access and start a new session');\n track.addEventListener('ended', ended);\n disposers.push(() => track.removeEventListener('ended', ended));\n if (track.readyState === 'ended')\n throw new SpeechConfigurationError(\n 'Microphone stream has ended',\n ADAPTER,\n );\n });\n if (!stream.getAudioTracks().length)\n throw new SpeechConfigurationError(\n 'Microphone stream has no audio tracks',\n ADAPTER,\n );\n peer = options.createPeerConnection?.() ?? new RTCPeerConnection();\n audio = options.audio ?? new Audio();\n audio.autoplay = true;\n audio.muted = outputMuted;\n peer.ontrack = (event) => {\n if (!audio || signal.aborted) return;\n audio.srcObject =\n event.streams[0] ?? new MediaStream([event.track]);\n void audio\n .play()\n .catch(() =>\n emit(\n 'error',\n new SpeechProviderError(\n ADAPTER,\n 'Audio playback needs a user gesture',\n ),\n ),\n );\n };\n peer.onconnectionstatechange = () => {\n if (\n peer?.connectionState === 'failed' ||\n peer?.connectionState === 'disconnected'\n )\n fail('Voice connection lost; start a new session');\n };\n for (const track of stream.getAudioTracks())\n peer.addTrack(track, stream);\n channel = peer.createDataChannel('oai-events');\n channel.onmessage = (event) => receive(event.data);\n // Attach before SDP negotiation; some transports open during setRemoteDescription.\n const opened = new Promise<void>((resolve, reject) => {\n const cancel = () =>\n reject(\n new SpeechProviderError(ADAPTER, 'Voice connection cancelled'),\n );\n signal.addEventListener('abort', cancel, { once: true });\n disposers.push(() => signal.removeEventListener('abort', cancel));\n if (channel) {\n channel.onopen = () => {\n signal.removeEventListener('abort', cancel);\n resolve();\n };\n channel.onclose = () => {\n fail('Voice channel closed');\n reject(\n new SpeechProviderError(ADAPTER, 'Voice channel closed'),\n );\n };\n channel.onerror = () => {\n fail('Voice channel failed');\n reject(\n new SpeechProviderError(ADAPTER, 'Voice channel failed'),\n );\n };\n }\n });\n // Keep an early rejection handled while negotiation is pending.\n void opened.catch(() => undefined);\n const offer = await abortable(peer.createOffer(), signal);\n await abortable(peer.setLocalDescription(offer), signal);\n signal.throwIfAborted();\n if (!offer.sdp)\n throw new SpeechConfigurationError(\n 'WebRTC did not produce an SDP offer',\n ADAPTER,\n );\n const answer = await abortable(\n options.negotiate(offer.sdp, signal),\n signal,\n );\n signal.throwIfAborted();\n await abortable(\n peer.setRemoteDescription({ type: 'answer', sdp: answer }),\n signal,\n );\n await opened;\n signal.throwIfAborted();\n change('connected');\n } catch (error) {\n fail('Unable to connect voice session');\n throw error;\n } finally {\n clearTimeout(timeout);\n }\n })().finally(() => {\n pending = undefined;\n });\n return pending;\n },\n sendText(text) {\n const trimmed = text.trim();\n if (!trimmed || trimmed.length > 16_000)\n throw new SpeechConfigurationError(\n 'Text must contain 1–16000 characters',\n ADAPTER,\n );\n const id = `hv_${crypto.randomUUID().replaceAll('-', '').slice(0, 24)}`;\n send({\n type: 'conversation.item.create',\n item: {\n id,\n type: 'message',\n role: 'user',\n content: [{ type: 'input_text', text: trimmed }],\n },\n });\n transcript(id, 'user', trimmed, true);\n session.respond();\n return id;\n },\n commitInput(respond = true) {\n send({ type: 'input_audio_buffer.commit' });\n if (respond) session.respond();\n },\n respond(instructions) {\n send({\n type: 'response.create',\n ...(instructions ? { response: { instructions } } : {}),\n });\n },\n interrupt() {\n if (state !== 'connected') return;\n if (responseActive)\n send({\n type: 'response.cancel',\n ...(responseId ? { response_id: responseId } : {}),\n });\n send({ type: 'output_audio_buffer.clear' });\n playback(false);\n },\n setMicMuted(muted) {\n micMuted = muted;\n microphone?.getAudioTracks().forEach((track) => {\n track.enabled = !muted;\n });\n if (muted && state === 'connected')\n send({ type: 'input_audio_buffer.clear' });\n },\n setOutputMuted(muted) {\n outputMuted = muted;\n if (audio) audio.muted = muted;\n if (muted) playback(false);\n },\n submitToolResult(callId, result) {\n send({\n type: 'conversation.item.create',\n item: {\n type: 'function_call_output',\n call_id: callId,\n output: JSON.stringify(result),\n },\n });\n },\n close() {\n if (state === 'closed') return;\n state = 'closed';\n cleanup();\n emit('state', state);\n listeners.clear();\n },\n on(event, listener) {\n const set = listeners.get(event) ?? new Set();\n set.add(listener as (value: never) => void);\n listeners.set(event, set);\n return () => {\n set.delete(listener as (value: never) => void);\n };\n },\n };\n return session;\n}\n\nfunction record(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n}\n\nfunction abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {\n if (signal.aborted) return Promise.reject(signal.reason);\n return new Promise((resolve, reject) => {\n const cancel = () => reject(signal.reason);\n signal.addEventListener('abort', cancel, { once: true });\n promise\n .then(resolve, reject)\n .finally(() => signal.removeEventListener('abort', cancel));\n });\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
- import { C as SpeechProviderError, S as SpeechError, _ as mimeTypeToAudioExtension, a as TRANSCRIBER_ENV_KEYS, b as InvalidSpeechAdapterError, c as hasTranscriberEnv, d as parseOptionalInteger, f as readEnv, g as DEFAULT_MAX_AUDIO_BYTES, h as reportSpeechUsage, i as registeredOptionalTranscribers, l as mergeHeaderInits, m as audioSecondsFromProviderUsage, n as optionalTranscriberEntry, o as defaultEnv, p as resolveTranscriberConfig, s as hasAnyEnv, t as getOptionalTranscriberFactory, u as parseHeadersEnv, v as normalizeAudioInput, x as SpeechConfigurationError, y as normalizeMimeType } from "./chunks/registry-DfID41Ew.js";
1
+ import { i as SpeechProviderError, n as SpeechConfigurationError, r as SpeechError, t as InvalidSpeechAdapterError } from "./chunks/errors-CQBX3OPS.js";
2
+ import { i as toWebSocketUrl, n as messageDataToText, r as resolveWebSocketFactory, t as isBrowserRuntime } from "./chunks/websocket-CR9uXAuS.js";
3
+ import { _ as mimeTypeToAudioExtension, a as TRANSCRIBER_ENV_KEYS, c as hasTranscriberEnv, d as parseOptionalInteger, f as readEnv, g as DEFAULT_MAX_AUDIO_BYTES, h as reportSpeechUsage, i as registeredOptionalTranscribers, l as mergeHeaderInits, m as audioSecondsFromProviderUsage, n as optionalTranscriberEntry, o as defaultEnv, p as resolveTranscriberConfig, s as hasAnyEnv, t as getOptionalTranscriberFactory, u as parseHeadersEnv, v as normalizeAudioInput, y as normalizeMimeType } from "./chunks/registry-C_5BakmM.js";
2
4
  //#region src/shared/retry.ts
3
5
  /**
4
6
  * Retry with exponential backoff for speech provider calls. Retries only
@@ -375,46 +377,6 @@ function parseHeaderNumber(response, headerName) {
375
377
  return Number.isFinite(parsed) ? parsed : void 0;
376
378
  }
377
379
  //#endregion
378
- //#region src/shared/websocket.ts
379
- /**
380
- * Minimal WebSocket surface used by streaming adapters. Browser, Node 22+
381
- * (`globalThis.WebSocket`), Deno, Bun, and the `ws` package all satisfy it.
382
- */
383
- /** True in browsers and web workers, where API keys must never be used. */
384
- function isBrowserRuntime() {
385
- const scope = globalThis;
386
- if (scope.process?.versions?.node) return false;
387
- return typeof scope.document !== "undefined" || typeof scope.WorkerGlobalScope !== "undefined";
388
- }
389
- /**
390
- * Resolves a socket factory from explicit options, falling back to
391
- * `globalThis.WebSocket`. Headers are only passed when present, so standard
392
- * constructors that cannot send them are never handed an options object.
393
- */
394
- function resolveWebSocketFactory(adapter, options) {
395
- if (options.createWebSocket) return options.createWebSocket;
396
- const Ctor = options.WebSocket ?? globalThis.WebSocket;
397
- if (!Ctor) throw new SpeechConfigurationError("No WebSocket implementation available; pass `WebSocket` or `createWebSocket`", adapter);
398
- return (url, init) => Object.keys(init.headers).length > 0 ? new Ctor(url, {
399
- protocols: init.protocols,
400
- headers: init.headers
401
- }) : new Ctor(url, init.protocols);
402
- }
403
- /** Converts `http(s)://` to `ws(s)://`; leaves `ws(s)://` unchanged. */
404
- function toWebSocketUrl(url, adapter) {
405
- const parsed = new URL(url);
406
- if (parsed.protocol === "http:") parsed.protocol = "ws:";
407
- else if (parsed.protocol === "https:") parsed.protocol = "wss:";
408
- else if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") throw new SpeechConfigurationError(`Unsupported streaming URL protocol: ${parsed.protocol}`, adapter);
409
- return parsed.toString();
410
- }
411
- /** Converts a message payload to text; returns `undefined` for unsupported data. */
412
- function messageDataToText(data) {
413
- if (typeof data === "string") return data;
414
- if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
415
- if (ArrayBuffer.isView(data)) return new TextDecoder().decode(data);
416
- }
417
- //#endregion
418
380
  //#region src/shared/realtime-auth.ts
419
381
  /**
420
382
  * Credential handling shared by realtime (WebSocket) adapters.