@ai-ecoverse/pi-audio 1.4.0 → 1.6.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 +8 -0
- package/dist/audio.js +25 -0
- package/dist/audio.js.map +3 -3
- package/dist/faux/realtime.js +57 -12
- package/dist/faux/realtime.js.map +2 -2
- package/dist/index.js +5 -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/realtime.js +93 -0
- package/dist/realtime.js.map +3 -3
- package/dist/registry.js +9 -0
- package/dist/registry.js.map +2 -2
- package/dist/types/audio.d.ts +2 -0
- package/dist/types/faux/realtime.d.ts +6 -2
- package/dist/types/index.d.ts +2 -1
- 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/realtime.d.ts +38 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -21,8 +21,16 @@ 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
|
|
|
28
|
+
`connectRealtime` rejects a transport the model does not list, and a `resumeHandle` when `resumable` is false, with `VoiceRequestError`. `realtimeSocket(url, options, protocols)` keeps that url and copies `protocols`. When `options.gateway` is set it uses `gateway.url` and puts the subprotocol `cf-aig-authorization.<token>` first, which is how a browser sends a Cloudflare AI Gateway credential on the same realtime protocol.
|
|
29
|
+
|
|
30
|
+
`planDelivery` reads `RealtimeModel.delivery`. An `unsupported` mode throws `VoiceRequestError`. `when_idle` with `approximate` support waits while the model is producing output and runs on `response_done`, `interrupt`, or `close`. Native `when_idle` is sent immediately so the server can schedule it. `planTruncation` adds `{ itemId, audioEndMs }` only when `truncation` is `client` and `interrupt` was given what the listener heard.
|
|
31
|
+
|
|
32
|
+
`RealtimeLedger` meters one session. Input seconds come from `appendAudio`. Output seconds come from each `output_audio` event: PCM, mulaw, and alaw from the byte length, and a compressed encoding from character alignment. `sendText` and each delivered `inject` add one text message. Tool results do not. `close()` prices those meters with `priceAudio`. Sum that usage once. `response_done.usage` is the adapter's per-response figure, not a second copy of the session total.
|
|
33
|
+
|
|
26
34
|
## Development
|
|
27
35
|
|
|
28
36
|
```sh
|
package/dist/audio.js
CHANGED
|
@@ -1 +1,26 @@
|
|
|
1
|
+
const sampleWidth = {
|
|
2
|
+
pcm16: 2,
|
|
3
|
+
pcmf32: 4,
|
|
4
|
+
mulaw: 1,
|
|
5
|
+
alaw: 1,
|
|
6
|
+
opus: 0,
|
|
7
|
+
mp3: 0,
|
|
8
|
+
wav: 0,
|
|
9
|
+
flac: 0
|
|
10
|
+
};
|
|
11
|
+
function chunkSeconds(chunk) {
|
|
12
|
+
const width = sampleWidth[chunk.format.encoding];
|
|
13
|
+
if (width === 0 || chunk.format.sampleRate === 0) return 0;
|
|
14
|
+
return chunk.data.byteLength / (width * chunk.format.channels * chunk.format.sampleRate);
|
|
15
|
+
}
|
|
16
|
+
function alignmentSeconds(alignment) {
|
|
17
|
+
if (alignment === void 0) return 0;
|
|
18
|
+
let ms = 0;
|
|
19
|
+
for (const duration of alignment.durationMs) ms += duration;
|
|
20
|
+
return ms / 1e3;
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
alignmentSeconds,
|
|
24
|
+
chunkSeconds
|
|
25
|
+
};
|
|
1
26
|
//# sourceMappingURL=audio.js.map
|
package/dist/audio.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [],
|
|
4
|
-
"sourcesContent": [],
|
|
5
|
-
"mappings": "",
|
|
3
|
+
"sources": ["../src/audio.ts"],
|
|
4
|
+
"sourcesContent": ["export type AudioEncoding = 'pcm16' | 'pcmf32' | 'mulaw' | 'alaw' | 'opus' | 'mp3' | 'wav' | 'flac';\n\nexport interface AudioFormat {\n encoding: AudioEncoding;\n sampleRate: number;\n channels: 1 | 2;\n}\n\nexport interface AudioChunk {\n data: Uint8Array;\n format: AudioFormat;\n timestamp: number;\n}\n\nexport type AudioSource = AsyncIterable<AudioChunk>;\n\nexport type TextSpan =\n | { entryId: string; contentIndex: number; start: number; end: number }\n | { itemId: string; start: number; end: number };\n\nexport interface CharAlignment {\n startMs: number[];\n durationMs: number[];\n}\n\nexport interface AudioTag {\n span: TextSpan;\n alignment?: CharAlignment;\n}\n\nexport interface AudioSink {\n write(chunk: AudioChunk, tag?: AudioTag): void;\n flush(): void;\n readonly playedMs: number;\n onPlayback(listener: (tag: AudioTag, offsetMs: number) => void): () => void;\n}\n\nconst sampleWidth: Record<AudioEncoding, number> = {\n pcm16: 2,\n pcmf32: 4,\n mulaw: 1,\n alaw: 1,\n opus: 0,\n mp3: 0,\n wav: 0,\n flac: 0,\n};\n\nexport function chunkSeconds(chunk: AudioChunk): number {\n const width = sampleWidth[chunk.format.encoding];\n if (width === 0 || chunk.format.sampleRate === 0) return 0;\n return chunk.data.byteLength / (width * chunk.format.channels * chunk.format.sampleRate);\n}\n\nexport function alignmentSeconds(alignment: CharAlignment | undefined): number {\n if (alignment === undefined) return 0;\n let ms = 0;\n for (const duration of alignment.durationMs) ms += duration;\n return ms / 1000;\n}\n"],
|
|
5
|
+
"mappings": "AAqCA,MAAM,cAA6C;AAAA,EACjD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AACR;AAEO,SAAS,aAAa,OAA2B;AACtD,QAAM,QAAQ,YAAY,MAAM,OAAO,QAAQ;AAC/C,MAAI,UAAU,KAAK,MAAM,OAAO,eAAe,EAAG,QAAO;AACzD,SAAO,MAAM,KAAK,cAAc,QAAQ,MAAM,OAAO,WAAW,MAAM,OAAO;AAC/E;AAEO,SAAS,iBAAiB,WAA8C;AAC7E,MAAI,cAAc,OAAW,QAAO;AACpC,MAAI,KAAK;AACT,aAAW,YAAY,UAAU,WAAY,OAAM;AACnD,SAAO,KAAK;AACd;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/faux/realtime.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { VoiceRequestError } from "../errors.js";
|
|
1
2
|
import {
|
|
2
3
|
registerVoiceModel
|
|
3
4
|
} from "../models.js";
|
|
5
|
+
import {
|
|
6
|
+
planTruncation,
|
|
7
|
+
RealtimeLedger,
|
|
8
|
+
realtimeSocket
|
|
9
|
+
} from "../realtime.js";
|
|
4
10
|
import { registerRealtimeApiProvider } from "../registry.js";
|
|
5
|
-
import { priceAudio } from "../usage.js";
|
|
6
11
|
import { modelKey, pcm, shareApi, zeroCost } from "./shared.js";
|
|
7
12
|
class FauxSession {
|
|
8
13
|
model;
|
|
9
14
|
#calls;
|
|
15
|
+
#ledger = new RealtimeLedger();
|
|
10
16
|
#queue = [];
|
|
11
17
|
#waiters = [];
|
|
12
18
|
#closed = false;
|
|
@@ -17,44 +23,79 @@ class FauxSession {
|
|
|
17
23
|
get closed() {
|
|
18
24
|
return this.#closed;
|
|
19
25
|
}
|
|
26
|
+
#assertOpen() {
|
|
27
|
+
if (this.#closed) throw new VoiceRequestError("error", "session is closed");
|
|
28
|
+
}
|
|
20
29
|
push(event) {
|
|
21
30
|
if (this.#closed) return;
|
|
31
|
+
this.#ledger.observe(event);
|
|
22
32
|
const waiter = this.#waiters.shift();
|
|
23
33
|
if (waiter) waiter({ value: event, done: false });
|
|
24
34
|
else this.#queue.push(event);
|
|
25
35
|
}
|
|
26
36
|
appendAudio(chunk) {
|
|
37
|
+
this.#assertOpen();
|
|
38
|
+
this.#ledger.addInput(chunk);
|
|
27
39
|
this.#calls.push({ method: "appendAudio", chunk });
|
|
28
40
|
}
|
|
29
41
|
appendVideoFrame(jpeg) {
|
|
42
|
+
this.#assertOpen();
|
|
43
|
+
if (this.model.video === void 0)
|
|
44
|
+
throw new VoiceRequestError("error", "video is unsupported");
|
|
30
45
|
this.#calls.push({ method: "appendVideoFrame", jpeg });
|
|
31
46
|
}
|
|
32
47
|
sendText(text) {
|
|
48
|
+
this.#assertOpen();
|
|
49
|
+
this.#ledger.addText();
|
|
33
50
|
this.#calls.push({ method: "sendText", text });
|
|
34
51
|
}
|
|
35
52
|
inject(text, delivery) {
|
|
36
|
-
this.#
|
|
53
|
+
this.#assertOpen();
|
|
54
|
+
const table = this.model.delivery;
|
|
55
|
+
this.#ledger.submit(table, delivery, () => {
|
|
56
|
+
this.#assertOpen();
|
|
57
|
+
this.#ledger.addText();
|
|
58
|
+
this.#calls.push({ method: "inject", text, delivery, support: table[delivery] });
|
|
59
|
+
});
|
|
37
60
|
}
|
|
38
61
|
respond(callId, result, delivery) {
|
|
39
|
-
this.#
|
|
62
|
+
this.#assertOpen();
|
|
63
|
+
const send = (support) => {
|
|
64
|
+
this.#calls.push({ method: "respond", callId, result, delivery, support });
|
|
65
|
+
};
|
|
66
|
+
if (delivery === void 0) {
|
|
67
|
+
send();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const table = this.model.delivery;
|
|
71
|
+
this.#ledger.submit(table, delivery, () => {
|
|
72
|
+
this.#assertOpen();
|
|
73
|
+
send(table[delivery]);
|
|
74
|
+
});
|
|
40
75
|
}
|
|
41
76
|
interrupt(heard) {
|
|
42
|
-
this.#
|
|
77
|
+
this.#assertOpen();
|
|
78
|
+
this.#calls.push({
|
|
79
|
+
method: "interrupt",
|
|
80
|
+
heard,
|
|
81
|
+
plan: planTruncation(this.model.truncation, heard)
|
|
82
|
+
});
|
|
83
|
+
this.#ledger.settle();
|
|
43
84
|
}
|
|
44
85
|
update(patch) {
|
|
86
|
+
this.#assertOpen();
|
|
87
|
+
if (patch.tools !== void 0 && !this.model.liveToolUpdate) {
|
|
88
|
+
throw new VoiceRequestError("error", "live tool update is unsupported");
|
|
89
|
+
}
|
|
45
90
|
this.#calls.push({ method: "update", patch });
|
|
46
91
|
}
|
|
47
92
|
async close() {
|
|
93
|
+
if (this.#closed) return { usage: this.#ledger.usage(this.model.audioCost) };
|
|
94
|
+
this.#ledger.settle();
|
|
48
95
|
this.#calls.push({ method: "close" });
|
|
49
96
|
this.#closed = true;
|
|
50
97
|
while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });
|
|
51
|
-
return {
|
|
52
|
-
usage: priceAudio(this.model.audioCost, {
|
|
53
|
-
inputSeconds: 0,
|
|
54
|
-
outputSeconds: 0,
|
|
55
|
-
textMessages: 0
|
|
56
|
-
})
|
|
57
|
-
};
|
|
98
|
+
return { usage: this.#ledger.usage(this.model.audioCost) };
|
|
58
99
|
}
|
|
59
100
|
async *[Symbol.asyncIterator]() {
|
|
60
101
|
while (true) {
|
|
@@ -111,6 +152,9 @@ function registerFauxRealtime(options = {}) {
|
|
|
111
152
|
return {
|
|
112
153
|
model,
|
|
113
154
|
calls: state.calls,
|
|
155
|
+
get socket() {
|
|
156
|
+
return state.socket;
|
|
157
|
+
},
|
|
114
158
|
emit(event) {
|
|
115
159
|
if (state.session) state.session.push(event);
|
|
116
160
|
if (!state.session || state.session.closed) state.pending.push(event);
|
|
@@ -127,9 +171,10 @@ function registerFauxRealtime(options = {}) {
|
|
|
127
171
|
}
|
|
128
172
|
};
|
|
129
173
|
}
|
|
130
|
-
async function connectFaux(model, _context,
|
|
174
|
+
async function connectFaux(model, _context, options) {
|
|
131
175
|
const state = states.get(modelKey(model));
|
|
132
176
|
if (!state) throw new Error(`Unknown faux realtime model ${model.provider}/${model.id}`);
|
|
177
|
+
state.socket = realtimeSocket(model.baseUrl, options, ["realtime"]);
|
|
133
178
|
const session = new FauxSession(state.model, state.calls);
|
|
134
179
|
state.session = session;
|
|
135
180
|
for (const event of state.pending) session.push(event);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/faux/realtime.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport {\n type Delivery,\n type DeliverySupport,\n type RealtimeModel,\n registerVoiceModel,\n} from '../models.ts';\nimport
|
|
5
|
-
"mappings": "AAEA;AAAA,EAIE;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["import type { Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport {\n type Delivery,\n type DeliverySupport,\n type RealtimeModel,\n registerVoiceModel,\n} from '../models.ts';\nimport {\n planTruncation,\n type RealtimeContext,\n type RealtimeEvent,\n RealtimeLedger,\n type RealtimeOptions,\n type RealtimeSession,\n type RealtimeSocket,\n realtimeSocket,\n type TruncationPlan,\n} from '../realtime.ts';\nimport { registerRealtimeApiProvider } from '../registry.ts';\nimport type { RealtimeAudioCost } from '../usage.ts';\nimport { modelKey, pcm, shareApi, zeroCost } from './shared.ts';\n\nexport type FauxRealtimeCall =\n | { method: 'appendAudio'; chunk: AudioChunk }\n | { method: 'appendVideoFrame'; jpeg: Uint8Array }\n | { method: 'sendText'; text: string }\n | { method: 'inject'; text: string; delivery: Delivery; support: DeliverySupport }\n | {\n method: 'respond';\n callId: string;\n result: Parameters<RealtimeSession['respond']>[1];\n delivery?: Delivery;\n support?: DeliverySupport;\n }\n | {\n method: 'interrupt';\n heard?: { itemId: string; heardMs: number };\n plan: TruncationPlan;\n }\n | { method: 'update'; patch: Parameters<RealtimeSession['update']>[0] }\n | { method: 'close' };\n\nexport interface FauxRealtimeFlags {\n toolCalls?: RealtimeModel<string>['toolCalls'];\n delivery?: Record<Delivery, DeliverySupport>;\n truncation?: RealtimeModel<string>['truncation'];\n}\n\nexport interface FauxRealtimeOptions extends FauxRealtimeFlags {\n api?: string;\n provider?: string;\n id?: string;\n name?: string;\n events?: RealtimeEvent[];\n audioCost?: RealtimeAudioCost;\n}\n\nexport interface FauxRealtime {\n model: RealtimeModel<string>;\n calls: FauxRealtimeCall[];\n readonly socket: RealtimeSocket | undefined;\n emit(event: RealtimeEvent): void;\n setFlags(flags: FauxRealtimeFlags): void;\n unregister(): void;\n}\n\ninterface IteratorResultEvent {\n value?: RealtimeEvent;\n done: boolean;\n}\n\nclass FauxSession implements RealtimeSession {\n readonly model: RealtimeModel<string>;\n readonly #calls: FauxRealtimeCall[];\n readonly #ledger = new RealtimeLedger();\n readonly #queue: RealtimeEvent[] = [];\n readonly #waiters: Array<(result: IteratorResultEvent) => void> = [];\n #closed = false;\n\n constructor(model: RealtimeModel<string>, calls: FauxRealtimeCall[]) {\n this.model = model;\n this.#calls = calls;\n }\n\n get closed(): boolean {\n return this.#closed;\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new VoiceRequestError('error', 'session is closed');\n }\n\n push(event: RealtimeEvent): void {\n if (this.#closed) return;\n this.#ledger.observe(event);\n const waiter = this.#waiters.shift();\n if (waiter) waiter({ value: event, done: false });\n else this.#queue.push(event);\n }\n\n appendAudio(chunk: AudioChunk): void {\n this.#assertOpen();\n this.#ledger.addInput(chunk);\n this.#calls.push({ method: 'appendAudio', chunk });\n }\n\n appendVideoFrame(jpeg: Uint8Array): void {\n this.#assertOpen();\n if (this.model.video === undefined)\n throw new VoiceRequestError('error', 'video is unsupported');\n this.#calls.push({ method: 'appendVideoFrame', jpeg });\n }\n\n sendText(text: string): void {\n this.#assertOpen();\n this.#ledger.addText();\n this.#calls.push({ method: 'sendText', text });\n }\n\n inject(text: string, delivery: Delivery): void {\n this.#assertOpen();\n const table = this.model.delivery;\n this.#ledger.submit(table, delivery, () => {\n this.#assertOpen();\n this.#ledger.addText();\n this.#calls.push({ method: 'inject', text, delivery, support: table[delivery] });\n });\n }\n\n respond(\n callId: string,\n result: Parameters<RealtimeSession['respond']>[1],\n delivery?: Delivery\n ): void {\n this.#assertOpen();\n const send = (support?: DeliverySupport) => {\n this.#calls.push({ method: 'respond', callId, result, delivery, support });\n };\n if (delivery === undefined) {\n send();\n return;\n }\n const table = this.model.delivery;\n this.#ledger.submit(table, delivery, () => {\n this.#assertOpen();\n send(table[delivery]);\n });\n }\n\n interrupt(heard?: { itemId: string; heardMs: number }): void {\n this.#assertOpen();\n this.#calls.push({\n method: 'interrupt',\n heard,\n plan: planTruncation(this.model.truncation, heard),\n });\n this.#ledger.settle();\n }\n\n update(patch: Parameters<RealtimeSession['update']>[0]): void {\n this.#assertOpen();\n if (patch.tools !== undefined && !this.model.liveToolUpdate) {\n throw new VoiceRequestError('error', 'live tool update is unsupported');\n }\n this.#calls.push({ method: 'update', patch });\n }\n\n async close(): Promise<{ usage: Usage }> {\n if (this.#closed) return { usage: this.#ledger.usage(this.model.audioCost) };\n this.#ledger.settle();\n this.#calls.push({ method: 'close' });\n this.#closed = true;\n while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });\n return { usage: this.#ledger.usage(this.model.audioCost) };\n }\n\n async *[Symbol.asyncIterator](): AsyncIterator<RealtimeEvent> {\n while (true) {\n const queued = this.#queue.shift();\n if (queued) {\n yield queued;\n continue;\n }\n if (this.#closed) return;\n const next = await new Promise<IteratorResultEvent>((resolve) => this.#waiters.push(resolve));\n if (next.done || next.value === undefined) return;\n yield next.value;\n }\n }\n}\n\ninterface RealtimeState {\n model: RealtimeModel<string>;\n calls: FauxRealtimeCall[];\n events: RealtimeEvent[];\n pending: RealtimeEvent[];\n socket?: RealtimeSocket;\n session?: FauxSession;\n}\n\nconst states = new Map<string, RealtimeState>();\nconst apis = new Map<string, () => void>();\n\nconst nativeDelivery: Record<Delivery, DeliverySupport> = {\n interrupt: 'native',\n when_idle: 'native',\n silent: 'native',\n};\n\nexport function registerFauxRealtime(options: FauxRealtimeOptions = {}): FauxRealtime {\n const model: RealtimeModel<string> = {\n id: options.id ?? 'faux-realtime',\n name: options.name ?? 'Faux realtime',\n api: options.api ?? 'faux-realtime',\n provider: options.provider ?? 'faux',\n baseUrl: 'http://localhost:0',\n type: 'realtime',\n transports: ['websocket'],\n toolCalls: options.toolCalls ?? 'blocking',\n inputFormat: pcm(24000),\n outputFormat: pcm(24000),\n resumable: false,\n ephemeralAuth: false,\n voices: [{ id: 'faux', name: 'Faux' }],\n provisioning: 'none',\n delivery: options.delivery ?? nativeDelivery,\n truncation: options.truncation ?? 'client',\n liveToolUpdate: true,\n cost: zeroCost,\n audioCost: options.audioCost ?? {},\n };\n const state: RealtimeState = { model, calls: [], events: options.events ?? [], pending: [] };\n states.set(modelKey(model), state);\n const unregisterModel = registerVoiceModel(model);\n const release = shareApi(\n apis,\n model.api,\n () => registerRealtimeApiProvider({ api: model.api, connect: connectFaux }),\n () => [...states.values()].some((entry) => entry.model.api === model.api)\n );\n return {\n model,\n calls: state.calls,\n get socket() {\n return state.socket;\n },\n emit(event) {\n if (state.session) state.session.push(event);\n if (!state.session || state.session.closed) state.pending.push(event);\n },\n setFlags(flags) {\n if (flags.toolCalls) state.model.toolCalls = flags.toolCalls;\n if (flags.delivery) state.model.delivery = flags.delivery;\n if (flags.truncation) state.model.truncation = flags.truncation;\n },\n unregister() {\n if (states.get(modelKey(model)) === state) states.delete(modelKey(model));\n unregisterModel();\n release();\n },\n };\n}\n\nasync function connectFaux(\n model: RealtimeModel<string>,\n _context: RealtimeContext,\n options?: RealtimeOptions\n): Promise<RealtimeSession> {\n const state = states.get(modelKey(model));\n if (!state) throw new Error(`Unknown faux realtime model ${model.provider}/${model.id}`);\n state.socket = realtimeSocket(model.baseUrl, options, ['realtime']);\n const session = new FauxSession(state.model, state.calls);\n state.session = session;\n for (const event of state.pending) session.push(event);\n state.pending = [];\n for (const event of state.events) session.push(event);\n return session;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
listVoiceModels,
|
|
13
13
|
registerVoiceModel
|
|
14
14
|
} from "./models.js";
|
|
15
|
+
import { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from "./realtime.js";
|
|
15
16
|
import {
|
|
16
17
|
connectRealtime,
|
|
17
18
|
listen,
|
|
@@ -26,6 +27,7 @@ import { TranscriptionEventStream } from "./transcription.js";
|
|
|
26
27
|
import { addAudioCounters, priceAudio } from "./usage.js";
|
|
27
28
|
import { energyVad } from "./vad.js";
|
|
28
29
|
export {
|
|
30
|
+
RealtimeLedger,
|
|
29
31
|
SpeechEventStream,
|
|
30
32
|
TranscriptionEventStream,
|
|
31
33
|
VoiceRequestError,
|
|
@@ -40,7 +42,10 @@ export {
|
|
|
40
42
|
getVoiceModel,
|
|
41
43
|
listVoiceModels,
|
|
42
44
|
listen,
|
|
45
|
+
planDelivery,
|
|
46
|
+
planTruncation,
|
|
43
47
|
priceAudio,
|
|
48
|
+
realtimeSocket,
|
|
44
49
|
registerRealtimeApiProvider,
|
|
45
50
|
registerSpeechApiProvider,
|
|
46
51
|
registerTranscriptionApiProvider,
|
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';\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;
|
|
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 DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } 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;AAYP,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;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
|
+
}
|