@ai-ecoverse/pi-audio 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/chunk-speech.js +135 -0
- package/dist/chunk-speech.js.map +7 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/openai/catalog.js +126 -0
- package/dist/openai/catalog.js.map +7 -0
- package/dist/openai/index.js +5 -0
- package/dist/openai/index.js.map +7 -0
- package/dist/openai/listen.js +200 -0
- package/dist/openai/listen.js.map +7 -0
- package/dist/openai/speak.js +91 -0
- package/dist/openai/speak.js.map +7 -0
- package/dist/openai/transcribe.js +112 -0
- package/dist/openai/transcribe.js.map +7 -0
- package/dist/openai/wire.js +201 -0
- package/dist/openai/wire.js.map +7 -0
- package/dist/registry.js +4 -0
- package/dist/registry.js.map +2 -2
- package/dist/sentences.js +88 -0
- package/dist/sentences.js.map +7 -0
- package/dist/transcription.js.map +2 -2
- package/dist/types/chunk-speech.d.ts +4 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/openai/catalog.d.ts +1 -0
- package/dist/types/openai/index.d.ts +1 -0
- package/dist/types/openai/listen.d.ts +4 -0
- package/dist/types/openai/speak.d.ts +4 -0
- package/dist/types/openai/transcribe.d.ts +4 -0
- package/dist/types/openai/wire.d.ts +52 -0
- package/dist/types/sentences.d.ts +5 -0
- package/dist/types/transcription.d.ts +2 -0
- package/dist/types/vad-listen.d.ts +7 -0
- package/dist/types/vad.d.ts +13 -0
- package/dist/vad-listen.js +223 -0
- package/dist/vad-listen.js.map +7 -0
- package/dist/vad.js +57 -0
- package/dist/vad.js.map +7 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ addAudioCounters(total, usage);
|
|
|
21
21
|
|
|
22
22
|
pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatches in `patches/patches.json`.
|
|
23
23
|
|
|
24
|
+
`@ai-ecoverse/pi-audio/openai` registers OpenAI and OpenRouter speech adapters when `registerOpenAI()` is called. Batch transcription posts multipart audio to `/audio/transcriptions`. OpenRouter (`provider` `openrouter`, or a base URL on `openrouter.ai`) posts JSON `input_audio` instead. Speech posts JSON to `/audio/speech` and reads the raw audio response. OpenAI can return pcm, mp3, wav, opus, or flac. OpenRouter returns pcm or mp3. Realtime transcription uses a WebSocket at `/realtime?intent=transcription` for `gpt-realtime-whisper` and `gpt-live-transcribe`. The catalog key is type, provider, and id, so the batch model keeps the id `gpt-4o-transcribe`.
|
|
25
|
+
|
|
24
26
|
`convertAudio` and `convertAudioSource` convert between `AudioFormat`s: PCM16, float32, G.711 μ-law and A-law, mono and stereo, sample rate, and PCM16 WAV. Opus, MP3 and FLAC pass through only when the format does not change. `encodeAudioBase64` and `decodeAudioBase64` are the wire helpers for JSON protocols. `AudioChunk.data` stays binary. The same converter runs in an AudioWorklet from `@ai-ecoverse/pi-audio/worklet` (`AUDIO_CONVERT_PROCESSOR`, `registerAudioConvertWorklet`). Adapters declare `inputFormats` and `outputFormats` and do not resample on their own.
|
|
25
27
|
|
|
26
28
|
## Development
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { blankUsage } from "./blank-usage.js";
|
|
2
|
+
import { VoiceRequestError } from "./errors.js";
|
|
3
|
+
import { sentencesOf } from "./sentences.js";
|
|
4
|
+
import {
|
|
5
|
+
SpeechEventStream
|
|
6
|
+
} from "./speech.js";
|
|
7
|
+
import { addAudioCounters } from "./usage.js";
|
|
8
|
+
function shiftSpan(span, offset) {
|
|
9
|
+
if ("entryId" in span) {
|
|
10
|
+
return {
|
|
11
|
+
entryId: span.entryId,
|
|
12
|
+
contentIndex: span.contentIndex,
|
|
13
|
+
start: span.start + offset,
|
|
14
|
+
end: span.end + offset
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return { itemId: span.itemId, start: span.start + offset, end: span.end + offset };
|
|
18
|
+
}
|
|
19
|
+
function mergeUsage(usages) {
|
|
20
|
+
const total = blankUsage();
|
|
21
|
+
for (const usage of usages) {
|
|
22
|
+
total.input += usage.input;
|
|
23
|
+
total.output += usage.output;
|
|
24
|
+
total.cacheRead += usage.cacheRead;
|
|
25
|
+
total.cacheWrite += usage.cacheWrite;
|
|
26
|
+
total.totalTokens += usage.totalTokens;
|
|
27
|
+
total.cost.input += usage.cost.input;
|
|
28
|
+
total.cost.output += usage.cost.output;
|
|
29
|
+
total.cost.cacheRead += usage.cost.cacheRead;
|
|
30
|
+
total.cost.cacheWrite += usage.cost.cacheWrite;
|
|
31
|
+
total.cost.total += usage.cost.total;
|
|
32
|
+
if (usage.reasoning !== void 0) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
|
|
33
|
+
if (usage.cacheWrite1h !== void 0) {
|
|
34
|
+
total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;
|
|
35
|
+
}
|
|
36
|
+
addAudioCounters(total, usage);
|
|
37
|
+
}
|
|
38
|
+
return total;
|
|
39
|
+
}
|
|
40
|
+
function failure(error) {
|
|
41
|
+
if (error instanceof VoiceRequestError) {
|
|
42
|
+
return { type: "error", reason: error.reason, error: error.message };
|
|
43
|
+
}
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
return { type: "error", reason: "error", error: message };
|
|
46
|
+
}
|
|
47
|
+
async function playSentence(speak, model, input, sentence, offset, options, stream, usages, state) {
|
|
48
|
+
let inner;
|
|
49
|
+
try {
|
|
50
|
+
inner = speak(model, { ...input, text: sentence }, options);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
stream.push(failure(error));
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
let terminal = false;
|
|
56
|
+
for await (const event of inner) {
|
|
57
|
+
if (options?.signal?.aborted && event.type !== "error" && event.type !== "done") {
|
|
58
|
+
stream.push({ type: "error", reason: "aborted", error: "aborted" });
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (event.type === "start") {
|
|
62
|
+
if (!state.started) stream.push(event);
|
|
63
|
+
state.started = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (event.type === "audio") {
|
|
67
|
+
stream.push({ ...event, span: shiftSpan(event.span, offset) });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (event.type === "done") {
|
|
71
|
+
usages.push(event.usage);
|
|
72
|
+
terminal = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
stream.push(event);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (terminal) return true;
|
|
79
|
+
try {
|
|
80
|
+
usages.push(await inner.result());
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
stream.push(failure(error));
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function speakInSentences(speak, model, input, options) {
|
|
88
|
+
const stream = new SpeechEventStream();
|
|
89
|
+
void pump(speak, model, input, options, stream);
|
|
90
|
+
return stream;
|
|
91
|
+
}
|
|
92
|
+
async function pump(speak, model, input, options, stream) {
|
|
93
|
+
if (options?.signal?.aborted) {
|
|
94
|
+
stream.push({ type: "error", reason: "aborted", error: "aborted" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const usages = [];
|
|
98
|
+
const state = { started: false };
|
|
99
|
+
let offset = 0;
|
|
100
|
+
let played = false;
|
|
101
|
+
try {
|
|
102
|
+
for await (const sentence of sentencesOf(input.text)) {
|
|
103
|
+
if (options?.signal?.aborted) {
|
|
104
|
+
stream.push({ type: "error", reason: "aborted", error: "aborted" });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
played = true;
|
|
108
|
+
const ok = await playSentence(
|
|
109
|
+
speak,
|
|
110
|
+
model,
|
|
111
|
+
input,
|
|
112
|
+
sentence,
|
|
113
|
+
offset,
|
|
114
|
+
options,
|
|
115
|
+
stream,
|
|
116
|
+
usages,
|
|
117
|
+
state
|
|
118
|
+
);
|
|
119
|
+
if (!ok) return;
|
|
120
|
+
offset += sentence.length;
|
|
121
|
+
}
|
|
122
|
+
if (!played) {
|
|
123
|
+
const text = typeof input.text === "string" ? input.text : "";
|
|
124
|
+
const ok = await playSentence(speak, model, input, text, 0, options, stream, usages, state);
|
|
125
|
+
if (!ok) return;
|
|
126
|
+
}
|
|
127
|
+
stream.push({ type: "done", usage: mergeUsage(usages) });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
stream.push(failure(error));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
speakInSentences
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=chunk-speech.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/chunk-speech.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions, Usage } from '@earendil-works/pi-ai';\nimport type { TextSpan } from './audio.ts';\nimport { blankUsage } from './blank-usage.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { SpeechModel } from './models.ts';\nimport { sentencesOf } from './sentences.ts';\nimport {\n type SpeechApiProvider,\n type SpeechEvent,\n SpeechEventStream,\n type SpeechInput,\n} from './speech.ts';\nimport { addAudioCounters } from './usage.ts';\n\nfunction shiftSpan(span: TextSpan, offset: number): TextSpan {\n if ('entryId' in span) {\n return {\n entryId: span.entryId,\n contentIndex: span.contentIndex,\n start: span.start + offset,\n end: span.end + offset,\n };\n }\n return { itemId: span.itemId, start: span.start + offset, end: span.end + offset };\n}\n\nfunction mergeUsage(usages: Usage[]): Usage {\n const total = blankUsage();\n for (const usage of usages) {\n total.input += usage.input;\n total.output += usage.output;\n total.cacheRead += usage.cacheRead;\n total.cacheWrite += usage.cacheWrite;\n total.totalTokens += usage.totalTokens;\n total.cost.input += usage.cost.input;\n total.cost.output += usage.cost.output;\n total.cost.cacheRead += usage.cost.cacheRead;\n total.cost.cacheWrite += usage.cost.cacheWrite;\n total.cost.total += usage.cost.total;\n if (usage.reasoning !== undefined) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;\n if (usage.cacheWrite1h !== undefined) {\n total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h;\n }\n addAudioCounters(total, usage);\n }\n return total;\n}\n\nfunction failure(error: unknown): SpeechEvent {\n if (error instanceof VoiceRequestError) {\n return { type: 'error', reason: error.reason, error: error.message };\n }\n const message = error instanceof Error ? error.message : String(error);\n return { type: 'error', reason: 'error', error: message };\n}\n\nasync function playSentence(\n speak: SpeechApiProvider['speak'],\n model: SpeechModel<string>,\n input: SpeechInput,\n sentence: string,\n offset: number,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream,\n usages: Usage[],\n state: { started: boolean }\n): Promise<boolean> {\n let inner: SpeechEventStream;\n try {\n inner = speak(model, { ...input, text: sentence }, options);\n } catch (error) {\n stream.push(failure(error));\n return false;\n }\n let terminal = false;\n for await (const event of inner) {\n if (options?.signal?.aborted && event.type !== 'error' && event.type !== 'done') {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return false;\n }\n if (event.type === 'start') {\n if (!state.started) stream.push(event);\n state.started = true;\n continue;\n }\n if (event.type === 'audio') {\n stream.push({ ...event, span: shiftSpan(event.span, offset) });\n continue;\n }\n if (event.type === 'done') {\n usages.push(event.usage);\n terminal = true;\n continue;\n }\n stream.push(event);\n return false;\n }\n if (terminal) return true;\n try {\n usages.push(await inner.result());\n return true;\n } catch (error) {\n stream.push(failure(error));\n return false;\n }\n}\n\nexport function speakInSentences(\n speak: SpeechApiProvider['speak'],\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n void pump(speak, model, input, options, stream);\n return stream;\n}\n\nasync function pump(\n speak: SpeechApiProvider['speak'],\n model: SpeechModel<string>,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n if (options?.signal?.aborted) {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n const usages: Usage[] = [];\n const state = { started: false };\n let offset = 0;\n let played = false;\n try {\n for await (const sentence of sentencesOf(input.text)) {\n if (options?.signal?.aborted) {\n stream.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n played = true;\n const ok = await playSentence(\n speak,\n model,\n input,\n sentence,\n offset,\n options,\n stream,\n usages,\n state\n );\n if (!ok) return;\n offset += sentence.length;\n }\n if (!played) {\n const text = typeof input.text === 'string' ? input.text : '';\n const ok = await playSentence(speak, model, input, text, 0, options, stream, usages, state);\n if (!ok) return;\n }\n stream.push({ type: 'done', usage: mergeUsage(usages) });\n } catch (error) {\n stream.push(failure(error));\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { SpeechEventStream } from "./speech.js";
|
|
25
25
|
import { TranscriptionEventStream } from "./transcription.js";
|
|
26
26
|
import { addAudioCounters, priceAudio } from "./usage.js";
|
|
27
|
+
import { energyVad } from "./vad.js";
|
|
27
28
|
export {
|
|
28
29
|
SpeechEventStream,
|
|
29
30
|
TranscriptionEventStream,
|
|
@@ -35,6 +36,7 @@ export {
|
|
|
35
36
|
convertAudioSource,
|
|
36
37
|
decodeAudioBase64,
|
|
37
38
|
encodeAudioBase64,
|
|
39
|
+
energyVad,
|
|
38
40
|
getVoiceModel,
|
|
39
41
|
listVoiceModels,
|
|
40
42
|
listen,
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeToken,\n} from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\n"],
|
|
5
|
-
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AASzC,SAAS,kBAAkB,kBAAkB;",
|
|
4
|
+
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeToken,\n} from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AASzC,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerVoiceModel
|
|
3
|
+
} from "../models.js";
|
|
4
|
+
import { registerSpeechApiProvider, registerTranscriptionApiProvider } from "../registry.js";
|
|
5
|
+
import { listenOpenAI } from "./listen.js";
|
|
6
|
+
import { speakOpenAI } from "./speak.js";
|
|
7
|
+
import { transcribeOpenAI } from "./transcribe.js";
|
|
8
|
+
const tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
9
|
+
const openai = "https://api.openai.com/v1";
|
|
10
|
+
const openrouter = "https://openrouter.ai/api/v1";
|
|
11
|
+
const pcm24 = { encoding: "pcm16", sampleRate: 24e3, channels: 1 };
|
|
12
|
+
const fileFormats = [
|
|
13
|
+
{ encoding: "wav", sampleRate: 16e3, channels: 1 },
|
|
14
|
+
{ encoding: "mp3", sampleRate: 16e3, channels: 1 },
|
|
15
|
+
{ encoding: "flac", sampleRate: 16e3, channels: 1 }
|
|
16
|
+
];
|
|
17
|
+
const openAISpeechFormats = [
|
|
18
|
+
pcm24,
|
|
19
|
+
{ encoding: "mp3", sampleRate: 24e3, channels: 1 },
|
|
20
|
+
{ encoding: "wav", sampleRate: 24e3, channels: 1 },
|
|
21
|
+
{ encoding: "opus", sampleRate: 24e3, channels: 1 },
|
|
22
|
+
{ encoding: "flac", sampleRate: 24e3, channels: 1 }
|
|
23
|
+
];
|
|
24
|
+
const openRouterSpeechFormats = [
|
|
25
|
+
pcm24,
|
|
26
|
+
{ encoding: "mp3", sampleRate: 24e3, channels: 1 }
|
|
27
|
+
];
|
|
28
|
+
const classicVoices = ["alloy", "ash", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer"];
|
|
29
|
+
const miniVoices = [...classicVoices, "ballad", "verse", "marin", "cedar"];
|
|
30
|
+
function voices(ids) {
|
|
31
|
+
return ids.map((id) => ({ id, name: id }));
|
|
32
|
+
}
|
|
33
|
+
function transcription(id, provider, baseUrl, perInputMinute, streaming, turnDetection, inputFormats) {
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
name: id,
|
|
37
|
+
api: streaming ? "openai-realtime-transcription" : "openai-transcriptions",
|
|
38
|
+
provider,
|
|
39
|
+
baseUrl,
|
|
40
|
+
type: "transcription",
|
|
41
|
+
streaming,
|
|
42
|
+
turnDetection,
|
|
43
|
+
inputFormats,
|
|
44
|
+
cost: tokenCost,
|
|
45
|
+
audioCost: { perInputMinute }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function speech(id, provider, baseUrl, outputFormats, voiceIds, audioCost) {
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
name: id,
|
|
52
|
+
api: "openai-speech",
|
|
53
|
+
provider,
|
|
54
|
+
baseUrl,
|
|
55
|
+
type: "speech",
|
|
56
|
+
textStreaming: false,
|
|
57
|
+
outputFormats,
|
|
58
|
+
voices: voices(voiceIds),
|
|
59
|
+
cloning: false,
|
|
60
|
+
cost: tokenCost,
|
|
61
|
+
audioCost
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function openAICatalog() {
|
|
65
|
+
return [
|
|
66
|
+
transcription("whisper-1", "openai", openai, 6e-3, false, "none", fileFormats),
|
|
67
|
+
transcription("gpt-4o-transcribe", "openai", openai, 6e-3, false, "none", fileFormats),
|
|
68
|
+
transcription("gpt-4o-mini-transcribe", "openai", openai, 3e-3, false, "none", fileFormats),
|
|
69
|
+
transcription("gpt-realtime-whisper", "openai", openai, 0.017, true, "vad", [pcm24]),
|
|
70
|
+
transcription("gpt-live-transcribe", "openai", openai, 0.017, true, "none", [pcm24]),
|
|
71
|
+
transcription("openai/whisper-1", "openrouter", openrouter, 6e-3, false, "none", fileFormats),
|
|
72
|
+
transcription(
|
|
73
|
+
"openai/whisper-large-v3",
|
|
74
|
+
"openrouter",
|
|
75
|
+
openrouter,
|
|
76
|
+
6e-3,
|
|
77
|
+
false,
|
|
78
|
+
"none",
|
|
79
|
+
fileFormats
|
|
80
|
+
),
|
|
81
|
+
speech("tts-1", "openai", openai, openAISpeechFormats, classicVoices, {
|
|
82
|
+
perThousandChars: 0.015
|
|
83
|
+
}),
|
|
84
|
+
speech("tts-1-hd", "openai", openai, openAISpeechFormats, classicVoices, {
|
|
85
|
+
perThousandChars: 0.03
|
|
86
|
+
}),
|
|
87
|
+
speech("gpt-4o-mini-tts", "openai", openai, openAISpeechFormats, miniVoices, {
|
|
88
|
+
perThousandChars: 15e-5,
|
|
89
|
+
perOutputMinute: 0.015
|
|
90
|
+
}),
|
|
91
|
+
speech(
|
|
92
|
+
"openai/gpt-4o-mini-tts-2025-12-15",
|
|
93
|
+
"openrouter",
|
|
94
|
+
openrouter,
|
|
95
|
+
openRouterSpeechFormats,
|
|
96
|
+
miniVoices,
|
|
97
|
+
{ perThousandChars: 15e-5, perOutputMinute: 0.015 }
|
|
98
|
+
)
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
function registerOpenAI() {
|
|
102
|
+
const models = openAICatalog();
|
|
103
|
+
const unregisterModels = models.map((model) => registerVoiceModel(model));
|
|
104
|
+
const unregisterBatch = registerTranscriptionApiProvider({
|
|
105
|
+
api: "openai-transcriptions",
|
|
106
|
+
transcribe: transcribeOpenAI
|
|
107
|
+
});
|
|
108
|
+
const unregisterRealtime = registerTranscriptionApiProvider({
|
|
109
|
+
api: "openai-realtime-transcription",
|
|
110
|
+
listen: listenOpenAI
|
|
111
|
+
});
|
|
112
|
+
const unregisterSpeech = registerSpeechApiProvider({ api: "openai-speech", speak: speakOpenAI });
|
|
113
|
+
let released = false;
|
|
114
|
+
return () => {
|
|
115
|
+
if (released) return;
|
|
116
|
+
released = true;
|
|
117
|
+
for (const unregister of unregisterModels) unregister();
|
|
118
|
+
unregisterBatch();
|
|
119
|
+
unregisterRealtime();
|
|
120
|
+
unregisterSpeech();
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
registerOpenAI
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/openai/catalog.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioFormat } from '../audio.ts';\nimport {\n registerVoiceModel,\n type SpeechModel,\n type TranscriptionModel,\n type VoiceInfo,\n} from '../models.ts';\nimport { registerSpeechApiProvider, registerTranscriptionApiProvider } from '../registry.ts';\nimport { listenOpenAI } from './listen.ts';\nimport { speakOpenAI } from './speak.ts';\nimport { transcribeOpenAI } from './transcribe.ts';\n\nconst tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\nconst openai = 'https://api.openai.com/v1';\nconst openrouter = 'https://openrouter.ai/api/v1';\n\nconst pcm24: AudioFormat = { encoding: 'pcm16', sampleRate: 24000, channels: 1 };\nconst fileFormats: AudioFormat[] = [\n { encoding: 'wav', sampleRate: 16000, channels: 1 },\n { encoding: 'mp3', sampleRate: 16000, channels: 1 },\n { encoding: 'flac', sampleRate: 16000, channels: 1 },\n];\nconst openAISpeechFormats: AudioFormat[] = [\n pcm24,\n { encoding: 'mp3', sampleRate: 24000, channels: 1 },\n { encoding: 'wav', sampleRate: 24000, channels: 1 },\n { encoding: 'opus', sampleRate: 24000, channels: 1 },\n { encoding: 'flac', sampleRate: 24000, channels: 1 },\n];\nconst openRouterSpeechFormats: AudioFormat[] = [\n pcm24,\n { encoding: 'mp3', sampleRate: 24000, channels: 1 },\n];\n\nconst classicVoices = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];\nconst miniVoices = [...classicVoices, 'ballad', 'verse', 'marin', 'cedar'];\n\nfunction voices(ids: string[]): VoiceInfo[] {\n return ids.map((id) => ({ id, name: id }));\n}\n\nfunction transcription(\n id: string,\n provider: string,\n baseUrl: string,\n perInputMinute: number,\n streaming: boolean,\n turnDetection: TranscriptionModel<string>['turnDetection'],\n inputFormats: AudioFormat[]\n): TranscriptionModel<string> {\n return {\n id,\n name: id,\n api: streaming ? 'openai-realtime-transcription' : 'openai-transcriptions',\n provider,\n baseUrl,\n type: 'transcription',\n streaming,\n turnDetection,\n inputFormats,\n cost: tokenCost,\n audioCost: { perInputMinute },\n };\n}\n\nfunction speech(\n id: string,\n provider: string,\n baseUrl: string,\n outputFormats: AudioFormat[],\n voiceIds: string[],\n audioCost: SpeechModel<string>['audioCost']\n): SpeechModel<string> {\n return {\n id,\n name: id,\n api: 'openai-speech',\n provider,\n baseUrl,\n type: 'speech',\n textStreaming: false,\n outputFormats,\n voices: voices(voiceIds),\n cloning: false,\n cost: tokenCost,\n audioCost,\n };\n}\n\nfunction openAICatalog(): Array<TranscriptionModel<string> | SpeechModel<string>> {\n return [\n transcription('whisper-1', 'openai', openai, 0.006, false, 'none', fileFormats),\n transcription('gpt-4o-transcribe', 'openai', openai, 0.006, false, 'none', fileFormats),\n transcription('gpt-4o-mini-transcribe', 'openai', openai, 0.003, false, 'none', fileFormats),\n transcription('gpt-realtime-whisper', 'openai', openai, 0.017, true, 'vad', [pcm24]),\n transcription('gpt-live-transcribe', 'openai', openai, 0.017, true, 'none', [pcm24]),\n transcription('openai/whisper-1', 'openrouter', openrouter, 0.006, false, 'none', fileFormats),\n transcription(\n 'openai/whisper-large-v3',\n 'openrouter',\n openrouter,\n 0.006,\n false,\n 'none',\n fileFormats\n ),\n speech('tts-1', 'openai', openai, openAISpeechFormats, classicVoices, {\n perThousandChars: 0.015,\n }),\n speech('tts-1-hd', 'openai', openai, openAISpeechFormats, classicVoices, {\n perThousandChars: 0.03,\n }),\n speech('gpt-4o-mini-tts', 'openai', openai, openAISpeechFormats, miniVoices, {\n perThousandChars: 0.00015,\n perOutputMinute: 0.015,\n }),\n speech(\n 'openai/gpt-4o-mini-tts-2025-12-15',\n 'openrouter',\n openrouter,\n openRouterSpeechFormats,\n miniVoices,\n { perThousandChars: 0.00015, perOutputMinute: 0.015 }\n ),\n ];\n}\n\nexport function registerOpenAI(): () => void {\n const models = openAICatalog();\n const unregisterModels = models.map((model) => registerVoiceModel(model));\n const unregisterBatch = registerTranscriptionApiProvider({\n api: 'openai-transcriptions',\n transcribe: transcribeOpenAI,\n });\n const unregisterRealtime = registerTranscriptionApiProvider({\n api: 'openai-realtime-transcription',\n listen: listenOpenAI,\n });\n const unregisterSpeech = registerSpeechApiProvider({ api: 'openai-speech', speak: speakOpenAI });\n let released = false;\n return () => {\n if (released) return;\n released = true;\n for (const unregister of unregisterModels) unregister();\n unregisterBatch();\n unregisterRealtime();\n unregisterSpeech();\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { encodeAudioBase64 } from "../convert.js";
|
|
2
|
+
import {
|
|
3
|
+
TranscriptionEventStream
|
|
4
|
+
} from "../transcription.js";
|
|
5
|
+
import { priceAudio } from "../usage.js";
|
|
6
|
+
import {
|
|
7
|
+
asVoiceError,
|
|
8
|
+
isRecord,
|
|
9
|
+
numberField,
|
|
10
|
+
realtimeUrl,
|
|
11
|
+
requestHeaders,
|
|
12
|
+
stringField
|
|
13
|
+
} from "./wire.js";
|
|
14
|
+
function turnDetection(model) {
|
|
15
|
+
if (model.turnDetection === "vad") return { type: "server_vad" };
|
|
16
|
+
if (model.turnDetection === "semantic") return { type: "semantic_vad" };
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function sessionBody(model, options, format) {
|
|
20
|
+
const transcription = { model: model.id };
|
|
21
|
+
if (options?.language) {
|
|
22
|
+
transcription.language = options.language;
|
|
23
|
+
transcription.languages = [options.language];
|
|
24
|
+
}
|
|
25
|
+
if (options?.vocabulary && options.vocabulary.length > 0)
|
|
26
|
+
transcription.keywords = options.vocabulary;
|
|
27
|
+
return {
|
|
28
|
+
type: "session.update",
|
|
29
|
+
session: {
|
|
30
|
+
type: "transcription",
|
|
31
|
+
audio: {
|
|
32
|
+
input: {
|
|
33
|
+
format: { type: "audio/pcm", rate: format.sampleRate },
|
|
34
|
+
transcription,
|
|
35
|
+
turn_detection: turnDetection(model)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function matches(chunk, format) {
|
|
42
|
+
return chunk.format.encoding === format.encoding && chunk.format.sampleRate === format.sampleRate && chunk.format.channels === format.channels;
|
|
43
|
+
}
|
|
44
|
+
async function messageText(data) {
|
|
45
|
+
if (typeof data === "string") return data;
|
|
46
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
47
|
+
if (data instanceof Uint8Array) return new TextDecoder().decode(data);
|
|
48
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return data.text();
|
|
49
|
+
throw new Error("Unsupported realtime message");
|
|
50
|
+
}
|
|
51
|
+
function applyEvent(event, state, push) {
|
|
52
|
+
const type = stringField(event, "type");
|
|
53
|
+
if (type === "input_audio_buffer.speech_started") {
|
|
54
|
+
const at = numberField(event, "audio_start_ms");
|
|
55
|
+
push({ type: "speech_start", at: at === void 0 ? 0 : at });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (type === "conversation.item.input_audio_transcription.delta") {
|
|
59
|
+
const item = stringField(event, "item_id");
|
|
60
|
+
state.segmentId = item === void 0 ? "0" : item;
|
|
61
|
+
const delta = stringField(event, "delta");
|
|
62
|
+
state.partial += delta === void 0 ? "" : delta;
|
|
63
|
+
push({ type: "partial", segmentId: state.segmentId, text: state.partial });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (type === "conversation.item.input_audio_transcription.completed") {
|
|
67
|
+
const transcript = stringField(event, "transcript");
|
|
68
|
+
const text = transcript === void 0 ? state.partial : transcript;
|
|
69
|
+
state.turns.push(text);
|
|
70
|
+
state.partial = "";
|
|
71
|
+
push({ type: "final", segmentId: state.segmentId, text });
|
|
72
|
+
push({ type: "turn_end", text, at: 0 });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (type === "error") {
|
|
76
|
+
const detail = event.error;
|
|
77
|
+
const message = isRecord(detail) ? stringField(detail, "message") : void 0;
|
|
78
|
+
state.failed = true;
|
|
79
|
+
push({
|
|
80
|
+
type: "error",
|
|
81
|
+
reason: "error",
|
|
82
|
+
error: message === void 0 ? "Realtime transcription failed" : message
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function listenOpenAI(model, audio, options) {
|
|
87
|
+
const stream = new TranscriptionEventStream();
|
|
88
|
+
void pumpListen(model, audio, options, stream);
|
|
89
|
+
return stream;
|
|
90
|
+
}
|
|
91
|
+
function openSocket(socket, fail) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
let opened = false;
|
|
94
|
+
socket.addEventListener("open", () => {
|
|
95
|
+
opened = true;
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
socket.addEventListener("error", () => {
|
|
99
|
+
fail("OpenAI realtime transcription socket failed");
|
|
100
|
+
if (!opened) resolve();
|
|
101
|
+
});
|
|
102
|
+
socket.addEventListener("close", () => {
|
|
103
|
+
if (!opened) {
|
|
104
|
+
fail("OpenAI realtime transcription socket closed");
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function pumpListen(model, audio, options, stream) {
|
|
111
|
+
const state = {
|
|
112
|
+
turns: [],
|
|
113
|
+
partial: "",
|
|
114
|
+
segmentId: "0",
|
|
115
|
+
bytes: 0,
|
|
116
|
+
audioDone: false,
|
|
117
|
+
failed: false
|
|
118
|
+
};
|
|
119
|
+
let finished = false;
|
|
120
|
+
const push = (event) => {
|
|
121
|
+
if (finished) return;
|
|
122
|
+
if (event.type === "done" || event.type === "error") finished = true;
|
|
123
|
+
stream.push(event);
|
|
124
|
+
};
|
|
125
|
+
let socket;
|
|
126
|
+
let wake;
|
|
127
|
+
const wait = new Promise((resolve) => {
|
|
128
|
+
wake = resolve;
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
132
|
+
const format = model.inputFormats[0];
|
|
133
|
+
if (!format) throw new Error("No input format");
|
|
134
|
+
requestHeaders(model, options);
|
|
135
|
+
const apiKey = options?.apiKey;
|
|
136
|
+
if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
|
|
137
|
+
socket = new WebSocket(realtimeUrl(model.baseUrl), [
|
|
138
|
+
"realtime",
|
|
139
|
+
`openai-insecure-api-key.${apiKey}`
|
|
140
|
+
]);
|
|
141
|
+
let incoming = Promise.resolve();
|
|
142
|
+
socket.addEventListener("message", (event) => {
|
|
143
|
+
incoming = incoming.then(() => onMessage(event.data, state, push, wake));
|
|
144
|
+
});
|
|
145
|
+
socket.addEventListener("close", () => wake?.());
|
|
146
|
+
await openSocket(socket, (message) => {
|
|
147
|
+
push({ type: "error", reason: "error", error: message });
|
|
148
|
+
wake?.();
|
|
149
|
+
});
|
|
150
|
+
if (finished) return;
|
|
151
|
+
socket.send(JSON.stringify(sessionBody(model, options, format)));
|
|
152
|
+
for await (const chunk of audio) {
|
|
153
|
+
if (finished) return;
|
|
154
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
155
|
+
if (!matches(chunk, format))
|
|
156
|
+
throw new Error(`Unsupported audio format ${chunk.format.encoding}`);
|
|
157
|
+
state.bytes += chunk.data.length;
|
|
158
|
+
socket.send(
|
|
159
|
+
JSON.stringify({ type: "input_audio_buffer.append", audio: encodeAudioBase64(chunk.data) })
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
state.audioDone = true;
|
|
163
|
+
if (model.turnDetection === "none") {
|
|
164
|
+
socket.send(JSON.stringify({ type: "input_audio_buffer.commit" }));
|
|
165
|
+
}
|
|
166
|
+
if (!finished && (model.turnDetection === "none" || state.turns.length === 0)) await wait;
|
|
167
|
+
if (finished) return;
|
|
168
|
+
const width = format.sampleRate * format.channels * 2;
|
|
169
|
+
const inputSeconds = width > 0 ? state.bytes / width : 0;
|
|
170
|
+
push({
|
|
171
|
+
type: "done",
|
|
172
|
+
transcript: {
|
|
173
|
+
text: state.turns.join(" "),
|
|
174
|
+
usage: priceAudio(model.audioCost, { inputSeconds })
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const voice = asVoiceError(error);
|
|
179
|
+
push({ type: "error", reason: voice.reason, error: voice.error });
|
|
180
|
+
} finally {
|
|
181
|
+
socket?.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function onMessage(data, state, push, wake) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(await messageText(data));
|
|
187
|
+
if (!isRecord(parsed)) throw new Error("Realtime transcription event was not JSON");
|
|
188
|
+
applyEvent(parsed, state, push);
|
|
189
|
+
if (state.failed || state.audioDone && state.turns.length > 0) wake?.();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const voice = asVoiceError(error);
|
|
192
|
+
state.failed = true;
|
|
193
|
+
push({ type: "error", reason: voice.reason, error: voice.error });
|
|
194
|
+
wake?.();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
listenOpenAI
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=listen.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/openai/listen.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioChunk, AudioFormat } from '../audio.ts';\nimport { encodeAudioBase64 } from '../convert.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport {\n type ListenOptions,\n type TranscriptionEvent,\n TranscriptionEventStream,\n} from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n asVoiceError,\n isRecord,\n numberField,\n realtimeUrl,\n requestHeaders,\n stringField,\n} from './wire.ts';\n\ninterface TurnState {\n turns: string[];\n partial: string;\n segmentId: string;\n bytes: number;\n audioDone: boolean;\n failed: boolean;\n}\n\nfunction turnDetection(model: TranscriptionModel<string>): Record<string, unknown> | null {\n if (model.turnDetection === 'vad') return { type: 'server_vad' };\n if (model.turnDetection === 'semantic') return { type: 'semantic_vad' };\n return null;\n}\n\nfunction sessionBody(\n model: TranscriptionModel<string>,\n options: ListenOptions | undefined,\n format: AudioFormat\n) {\n const transcription: Record<string, unknown> = { model: model.id };\n if (options?.language) {\n transcription.language = options.language;\n transcription.languages = [options.language];\n }\n if (options?.vocabulary && options.vocabulary.length > 0)\n transcription.keywords = options.vocabulary;\n return {\n type: 'session.update',\n session: {\n type: 'transcription',\n audio: {\n input: {\n format: { type: 'audio/pcm', rate: format.sampleRate },\n transcription,\n turn_detection: turnDetection(model),\n },\n },\n },\n };\n}\n\nfunction matches(chunk: AudioChunk, format: AudioFormat): boolean {\n return (\n chunk.format.encoding === format.encoding &&\n chunk.format.sampleRate === format.sampleRate &&\n chunk.format.channels === format.channels\n );\n}\n\nasync function messageText(data: unknown): Promise<string> {\n if (typeof data === 'string') return data;\n if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);\n if (data instanceof Uint8Array) return new TextDecoder().decode(data);\n if (typeof Blob !== 'undefined' && data instanceof Blob) return data.text();\n throw new Error('Unsupported realtime message');\n}\n\nfunction applyEvent(\n event: Record<string, unknown>,\n state: TurnState,\n push: (event: TranscriptionEvent) => void\n) {\n const type = stringField(event, 'type');\n if (type === 'input_audio_buffer.speech_started') {\n const at = numberField(event, 'audio_start_ms');\n push({ type: 'speech_start', at: at === undefined ? 0 : at });\n return;\n }\n if (type === 'conversation.item.input_audio_transcription.delta') {\n const item = stringField(event, 'item_id');\n state.segmentId = item === undefined ? '0' : item;\n const delta = stringField(event, 'delta');\n state.partial += delta === undefined ? '' : delta;\n push({ type: 'partial', segmentId: state.segmentId, text: state.partial });\n return;\n }\n if (type === 'conversation.item.input_audio_transcription.completed') {\n const transcript = stringField(event, 'transcript');\n const text = transcript === undefined ? state.partial : transcript;\n state.turns.push(text);\n state.partial = '';\n push({ type: 'final', segmentId: state.segmentId, text });\n push({ type: 'turn_end', text, at: 0 });\n return;\n }\n if (type === 'error') {\n const detail = event.error;\n const message = isRecord(detail) ? stringField(detail, 'message') : undefined;\n state.failed = true;\n push({\n type: 'error',\n reason: 'error',\n error: message === undefined ? 'Realtime transcription failed' : message,\n });\n }\n}\n\nexport function listenOpenAI(\n model: TranscriptionModel<string>,\n audio: AsyncIterable<AudioChunk>,\n options?: ListenOptions\n): TranscriptionEventStream {\n const stream = new TranscriptionEventStream();\n void pumpListen(model, audio, options, stream);\n return stream;\n}\n\nfunction openSocket(socket: WebSocket, fail: (message: string) => void): Promise<void> {\n return new Promise((resolve) => {\n let opened = false;\n socket.addEventListener('open', () => {\n opened = true;\n resolve();\n });\n socket.addEventListener('error', () => {\n fail('OpenAI realtime transcription socket failed');\n if (!opened) resolve();\n });\n socket.addEventListener('close', () => {\n if (!opened) {\n fail('OpenAI realtime transcription socket closed');\n resolve();\n }\n });\n });\n}\n\nasync function pumpListen(\n model: TranscriptionModel<string>,\n audio: AsyncIterable<AudioChunk>,\n options: ListenOptions | undefined,\n stream: TranscriptionEventStream\n) {\n const state: TurnState = {\n turns: [],\n partial: '',\n segmentId: '0',\n bytes: 0,\n audioDone: false,\n failed: false,\n };\n let finished = false;\n const push = (event: TranscriptionEvent) => {\n if (finished) return;\n if (event.type === 'done' || event.type === 'error') finished = true;\n stream.push(event);\n };\n let socket: WebSocket | undefined;\n let wake: (() => void) | undefined;\n const wait = new Promise<void>((resolve) => {\n wake = resolve;\n });\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const format = model.inputFormats[0];\n if (!format) throw new Error('No input format');\n requestHeaders(model, options);\n const apiKey = options?.apiKey;\n if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);\n socket = new WebSocket(realtimeUrl(model.baseUrl), [\n 'realtime',\n `openai-insecure-api-key.${apiKey}`,\n ]);\n let incoming = Promise.resolve();\n socket.addEventListener('message', (event) => {\n incoming = incoming.then(() => onMessage(event.data, state, push, wake));\n });\n socket.addEventListener('close', () => wake?.());\n await openSocket(socket, (message) => {\n push({ type: 'error', reason: 'error', error: message });\n wake?.();\n });\n if (finished) return;\n socket.send(JSON.stringify(sessionBody(model, options, format)));\n for await (const chunk of audio) {\n if (finished) return;\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n if (!matches(chunk, format))\n throw new Error(`Unsupported audio format ${chunk.format.encoding}`);\n state.bytes += chunk.data.length;\n socket.send(\n JSON.stringify({ type: 'input_audio_buffer.append', audio: encodeAudioBase64(chunk.data) })\n );\n }\n state.audioDone = true;\n if (model.turnDetection === 'none') {\n socket.send(JSON.stringify({ type: 'input_audio_buffer.commit' }));\n }\n if (!finished && (model.turnDetection === 'none' || state.turns.length === 0)) await wait;\n if (finished) return;\n const width = format.sampleRate * format.channels * 2;\n const inputSeconds = width > 0 ? state.bytes / width : 0;\n push({\n type: 'done',\n transcript: {\n text: state.turns.join(' '),\n usage: priceAudio(model.audioCost, { inputSeconds }),\n },\n });\n } catch (error) {\n const voice = asVoiceError(error);\n push({ type: 'error', reason: voice.reason, error: voice.error });\n } finally {\n socket?.close();\n }\n}\n\nasync function onMessage(\n data: unknown,\n state: TurnState,\n push: (event: TranscriptionEvent) => void,\n wake: (() => void) | undefined\n) {\n try {\n const parsed = JSON.parse(await messageText(data)) as unknown;\n if (!isRecord(parsed)) throw new Error('Realtime transcription event was not JSON');\n applyEvent(parsed, state, push);\n if (state.failed || (state.audioDone && state.turns.length > 0)) wake?.();\n } catch (error) {\n const voice = asVoiceError(error);\n state.failed = true;\n push({ type: 'error', reason: voice.reason, error: voice.error });\n wake?.();\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|