@ai-ecoverse/pi-audio 1.1.0 → 1.3.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 CHANGED
@@ -10,6 +10,19 @@ The package is a TypeScript ES module library for Node 24 and newer. It peers `@
10
10
 
11
11
  Transcription, speech, and realtime models are added to pi-ai's `ModelTypeMap` by declaration merging. They are not chat models: `Models.getAllModels()` does not list them. Register them with `registerVoiceModel` and look them up with `getVoiceModel` / `listVoiceModels`. Operations dispatch on `model.api` through `registerTranscriptionApiProvider`, `registerSpeechApiProvider`, and `registerRealtimeApiProvider`. A stored chat credential is reused with `resolveVoiceAuth(models, voiceModel)`, which calls `Models.getAuth` for that provider. Provider adapters ship as their own subpath exports (`@ai-ecoverse/pi-audio/openai`, and the rest) so a host loads only the ones it uses. `@ai-ecoverse/pi-audio/faux` registers scripted transcription, speech, and realtime adapters for tests.
12
12
 
13
+ A voice call reports pi-ai `Usage` with an optional `audio` field: `inputSeconds`, `outputSeconds`, `characters`, and `textMessages`. `AudioUsage` is that `Usage`. Token counters stay at 0. Dollars go in `cost.input`, `cost.output`, and `cost.total`, priced from the model's `audioCost` (`perInputMinute`, `perOutputMinute`, `perThousandChars`, `perTextMessage`) by `priceAudio`. Input seconds and text messages land in `cost.input`. Output seconds and characters land in `cost.output`.
14
+
15
+ pi-durable's `addUsage` (in `packages/durable/src/harness/usage.ts`, not exported from the package) sums a fixed list of token and cost fields and drops `audio`. A host that folds voice usage through pi-durable patches `addUsage` to call `addAudioCounters(total, usage)` beside the reasoning line:
16
+
17
+ ```ts
18
+ if (usage.reasoning !== undefined) total.reasoning = (total.reasoning ?? 0) + usage.reasoning;
19
+ addAudioCounters(total, usage);
20
+ ```
21
+
22
+ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatches in `patches/patches.json`.
23
+
24
+ `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
+
13
26
  ## Development
14
27
 
15
28
  ```sh
@@ -0,0 +1,294 @@
1
+ const compressed = /* @__PURE__ */ new Set(["opus", "mp3", "flac"]);
2
+ const LAW_BIAS = 132;
3
+ const base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4
+ function byteAt(data, index) {
5
+ const byte = data[index];
6
+ return byte === void 0 ? 0 : byte;
7
+ }
8
+ function symbolAt(index) {
9
+ return base64Alphabet.charAt(index);
10
+ }
11
+ function encodeAudioBase64(data) {
12
+ let encoded = "";
13
+ for (let index = 0; index < data.length; index += 3) {
14
+ const second = data[index + 1];
15
+ const third = data[index + 2];
16
+ const triple = byteAt(data, index) << 16 | byteAt(data, index + 1) << 8 | byteAt(data, index + 2);
17
+ encoded += symbolAt(triple >> 18 & 63);
18
+ encoded += symbolAt(triple >> 12 & 63);
19
+ encoded += second === void 0 ? "=" : symbolAt(triple >> 6 & 63);
20
+ encoded += third === void 0 ? "=" : symbolAt(triple & 63);
21
+ }
22
+ return encoded;
23
+ }
24
+ function codeAt(value, index) {
25
+ const character = value[index];
26
+ if (character === void 0) return -1;
27
+ const code = base64Alphabet.indexOf(character);
28
+ if (code < 0) throw new Error("Invalid base64 audio");
29
+ return code;
30
+ }
31
+ function sextet(code) {
32
+ return code < 0 ? 0 : code;
33
+ }
34
+ function decodeAudioBase64(value) {
35
+ const cleaned = value.replaceAll("=", "");
36
+ if (cleaned.length % 4 === 1) throw new Error("Invalid base64 audio");
37
+ const bytes = [];
38
+ for (let index = 0; index < cleaned.length; index += 4) {
39
+ const first = codeAt(cleaned, index);
40
+ const second = codeAt(cleaned, index + 1);
41
+ const third = codeAt(cleaned, index + 2);
42
+ const fourth = codeAt(cleaned, index + 3);
43
+ const triple = sextet(first) << 18 | sextet(second) << 12 | sextet(third) << 6 | sextet(fourth);
44
+ bytes.push(triple >> 16 & 255);
45
+ if (third >= 0) bytes.push(triple >> 8 & 255);
46
+ if (fourth >= 0) bytes.push(triple & 255);
47
+ }
48
+ return Uint8Array.from(bytes);
49
+ }
50
+ function convertAudio(chunk, format) {
51
+ checkFormat(chunk.format);
52
+ checkFormat(format);
53
+ if (sameFormat(chunk.format, format)) {
54
+ return { data: chunk.data.slice(), format, timestamp: chunk.timestamp };
55
+ }
56
+ if (compressed.has(chunk.format.encoding) || compressed.has(format.encoding)) {
57
+ throw new Error(
58
+ `Cannot convert ${chunk.format.encoding} to ${format.encoding} without a codec`
59
+ );
60
+ }
61
+ const decoded = decode(chunk.data, chunk.format);
62
+ const mixed = mix(decoded.channels, format.channels);
63
+ const resampled = mixed.map(
64
+ (channel) => resample(channel, decoded.sampleRate, format.sampleRate)
65
+ );
66
+ return {
67
+ data: encode(resampled, format),
68
+ format,
69
+ timestamp: chunk.timestamp
70
+ };
71
+ }
72
+ async function* convertAudioSource(source, format) {
73
+ for await (const chunk of source) yield convertAudio(chunk, format);
74
+ }
75
+ function sameFormat(left, right) {
76
+ return left.encoding === right.encoding && left.sampleRate === right.sampleRate && left.channels === right.channels;
77
+ }
78
+ function checkFormat(format) {
79
+ if (format.channels !== 1 && format.channels !== 2) {
80
+ throw new Error(`Unsupported channel count ${String(format.channels)}`);
81
+ }
82
+ if (!Number.isFinite(format.sampleRate) || format.sampleRate <= 0) {
83
+ throw new Error(`Unsupported sample rate ${String(format.sampleRate)}`);
84
+ }
85
+ }
86
+ function decode(data, format) {
87
+ if (format.encoding === "wav") return decodeWav(data);
88
+ return { channels: decodePcm(data, format), sampleRate: format.sampleRate };
89
+ }
90
+ function decodePcm(data, format) {
91
+ const width = sampleWidth(format.encoding);
92
+ const frames = Math.floor(data.byteLength / (width * format.channels));
93
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
94
+ const channels = Array.from({ length: format.channels }, () => new Float32Array(frames));
95
+ for (let frame = 0; frame < frames; frame++) {
96
+ for (let channel = 0; channel < format.channels; channel++) {
97
+ const offset = (frame * format.channels + channel) * width;
98
+ channels[channel][frame] = readSample(view, offset, format.encoding);
99
+ }
100
+ }
101
+ return channels;
102
+ }
103
+ function sampleWidth(encoding) {
104
+ if (encoding === "pcm16") return 2;
105
+ if (encoding === "pcmf32") return 4;
106
+ if (encoding === "mulaw" || encoding === "alaw") return 1;
107
+ throw new Error(`Unsupported audio encoding ${encoding}`);
108
+ }
109
+ function readSample(view, offset, encoding) {
110
+ if (encoding === "pcm16") return int16ToFloat(view.getInt16(offset, true));
111
+ if (encoding === "pcmf32") return finite(view.getFloat32(offset, true));
112
+ if (encoding === "mulaw") return int16ToFloat(mulawToLinear(view.getUint8(offset)));
113
+ return int16ToFloat(alawToLinear(view.getUint8(offset)));
114
+ }
115
+ function encode(channels, format) {
116
+ if (format.encoding === "wav") {
117
+ const pcm = encode(channels, { ...format, encoding: "pcm16" });
118
+ return wavBytes(pcm, format.sampleRate, format.channels);
119
+ }
120
+ const width = sampleWidth(format.encoding);
121
+ const frames = channels[0]?.length ?? 0;
122
+ const bytes = new Uint8Array(frames * format.channels * width);
123
+ const view = new DataView(bytes.buffer);
124
+ for (let frame = 0; frame < frames; frame++) {
125
+ for (let channel = 0; channel < format.channels; channel++) {
126
+ const offset = (frame * format.channels + channel) * width;
127
+ writeSample(view, offset, format.encoding, channels[channel]?.[frame] ?? 0);
128
+ }
129
+ }
130
+ return bytes;
131
+ }
132
+ function writeSample(view, offset, encoding, sample) {
133
+ const unit = finite(sample);
134
+ if (encoding === "pcm16") view.setInt16(offset, floatToInt16(unit), true);
135
+ else if (encoding === "pcmf32") view.setFloat32(offset, unit, true);
136
+ else if (encoding === "mulaw") view.setUint8(offset, linearToMulaw(floatToInt16(unit)));
137
+ else view.setUint8(offset, linearToAlaw(floatToInt16(unit)));
138
+ }
139
+ function mix(channels, target) {
140
+ const length = channels[0]?.length ?? 0;
141
+ if (channels.length === target) return channels;
142
+ if (target === 1) {
143
+ const mixed = new Float32Array(length);
144
+ if (channels.length === 0) return [mixed];
145
+ for (let index = 0; index < length; index++) {
146
+ let sum = 0;
147
+ for (const channel of channels) sum += channel[index] ?? 0;
148
+ mixed[index] = sum / channels.length;
149
+ }
150
+ return [mixed];
151
+ }
152
+ const left = channels[0] ?? new Float32Array(length);
153
+ return [left, channels[1] ?? left];
154
+ }
155
+ function resample(input, inRate, outRate) {
156
+ if (inRate === outRate || input.length === 0) return input;
157
+ const outLength = Math.max(1, Math.round(input.length * outRate / inRate));
158
+ const output = new Float32Array(outLength);
159
+ const scale = inRate / outRate;
160
+ const last = input.length - 1;
161
+ for (let index = 0; index < outLength; index++) {
162
+ const position = index * scale;
163
+ const cursor = Math.min(Math.floor(position), last);
164
+ const next = Math.min(cursor + 1, last);
165
+ const fraction = position - cursor;
166
+ const left = input[cursor] ?? 0;
167
+ const right = input[next] ?? 0;
168
+ output[index] = left + (right - left) * fraction;
169
+ }
170
+ return output;
171
+ }
172
+ function finite(sample) {
173
+ return Number.isFinite(sample) ? sample : 0;
174
+ }
175
+ function floatToInt16(sample) {
176
+ const clamped = Math.max(-1, Math.min(1, sample));
177
+ const scaled = clamped < 0 ? clamped * 32768 : clamped * 32767;
178
+ return Math.max(-32768, Math.min(32767, Math.round(scaled)));
179
+ }
180
+ function int16ToFloat(sample) {
181
+ return sample < 0 ? sample / 32768 : sample / 32767;
182
+ }
183
+ function mulawToLinear(mulaw) {
184
+ const value = ~mulaw;
185
+ const exponent = (value & 112) >> 4;
186
+ let sample = ((value & 15) << 3) + LAW_BIAS;
187
+ sample <<= exponent;
188
+ return (value & 128) !== 0 ? LAW_BIAS - sample : sample - LAW_BIAS;
189
+ }
190
+ function alawToLinear(alaw) {
191
+ const value = alaw ^ 85;
192
+ const exponent = (value & 112) >> 4;
193
+ const mantissa = value & 15;
194
+ const sample = exponent === 0 ? mantissa + mantissa + 1 << 3 : mantissa + mantissa + 1 + 32 << exponent + 2;
195
+ return (value & 128) !== 0 ? sample : -sample;
196
+ }
197
+ function buildLawTable(expand, mask) {
198
+ const table = new Uint8Array(16384);
199
+ table[8192] = mask;
200
+ let cursor = 1;
201
+ for (let code = 0; code < 127; code++) {
202
+ const left = expand(code ^ mask);
203
+ const right = expand(code + 1 ^ mask);
204
+ const limit = left + right + 4 >> 3;
205
+ for (; cursor < limit; cursor += 1) {
206
+ table[8192 - cursor] = (code ^ (mask ^ 128)) & 255;
207
+ table[8192 + cursor] = (code ^ mask) & 255;
208
+ }
209
+ }
210
+ for (; cursor < 8192; cursor++) {
211
+ table[8192 - cursor] = (127 ^ (mask ^ 128)) & 255;
212
+ table[8192 + cursor] = (127 ^ mask) & 255;
213
+ }
214
+ table[0] = table[1] ?? mask;
215
+ return table;
216
+ }
217
+ const mulawTable = buildLawTable(mulawToLinear, 255);
218
+ const alawTable = buildLawTable(alawToLinear, 213);
219
+ function lawCode(table, pcm, fallback) {
220
+ return table[pcm + 32768 >> 2] ?? fallback;
221
+ }
222
+ function linearToMulaw(pcm) {
223
+ return lawCode(mulawTable, pcm, 255);
224
+ }
225
+ function linearToAlaw(pcm) {
226
+ return lawCode(alawTable, pcm, 213);
227
+ }
228
+ function decodeWav(data) {
229
+ if (data.byteLength < 44) throw new Error("Invalid WAV audio");
230
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
231
+ if (text(data, 0, 4) !== "RIFF" || text(data, 8, 4) !== "WAVE")
232
+ throw new Error("Invalid WAV audio");
233
+ let cursor = 12;
234
+ let format;
235
+ let payload;
236
+ while (cursor + 8 <= data.byteLength) {
237
+ const id = text(data, cursor, 4);
238
+ const size = view.getUint32(cursor + 4, true);
239
+ const start = cursor + 8;
240
+ const end = Math.min(start + size, data.byteLength);
241
+ if (id === "fmt " && end - start >= 16) {
242
+ format = {
243
+ audioFormat: view.getUint16(start, true),
244
+ channels: view.getUint16(start + 2, true),
245
+ sampleRate: view.getUint32(start + 4, true),
246
+ width: view.getUint16(start + 14, true) / 8
247
+ };
248
+ } else if (id === "data") payload = data.subarray(start, end);
249
+ cursor = start + size + size % 2;
250
+ }
251
+ if (!format || !payload) throw new Error("Invalid WAV audio");
252
+ if (format.audioFormat !== 1 || format.width !== 2) throw new Error("WAV audio must be PCM16");
253
+ if (format.channels !== 1 && format.channels !== 2) throw new Error("Invalid WAV audio");
254
+ return {
255
+ channels: decodePcm(payload, {
256
+ encoding: "pcm16",
257
+ sampleRate: format.sampleRate,
258
+ channels: format.channels
259
+ }),
260
+ sampleRate: format.sampleRate
261
+ };
262
+ }
263
+ function wavBytes(pcm, sampleRate, channels) {
264
+ const bytes = new Uint8Array(44 + pcm.byteLength);
265
+ const view = new DataView(bytes.buffer);
266
+ bytes.set(ascii("RIFF"), 0);
267
+ view.setUint32(4, 36 + pcm.byteLength, true);
268
+ bytes.set(ascii("WAVE"), 8);
269
+ bytes.set(ascii("fmt "), 12);
270
+ view.setUint32(16, 16, true);
271
+ view.setUint16(20, 1, true);
272
+ view.setUint16(22, channels, true);
273
+ view.setUint32(24, sampleRate, true);
274
+ view.setUint32(28, sampleRate * channels * 2, true);
275
+ view.setUint16(32, channels * 2, true);
276
+ view.setUint16(34, 16, true);
277
+ bytes.set(ascii("data"), 36);
278
+ view.setUint32(40, pcm.byteLength, true);
279
+ bytes.set(pcm, 44);
280
+ return bytes;
281
+ }
282
+ function text(data, offset, length) {
283
+ return String.fromCharCode(...data.subarray(offset, offset + length));
284
+ }
285
+ function ascii(value) {
286
+ return Uint8Array.from(value, (character) => character.charCodeAt(0));
287
+ }
288
+ export {
289
+ convertAudio,
290
+ convertAudioSource,
291
+ decodeAudioBase64,
292
+ encodeAudioBase64
293
+ };
294
+ //# sourceMappingURL=convert.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/convert.ts"],
4
+ "sourcesContent": ["import type { AudioChunk, AudioEncoding, AudioFormat, AudioSource } from './audio.ts';\n\nconst compressed = new Set<AudioEncoding>(['opus', 'mp3', 'flac']);\nconst LAW_BIAS = 0x84;\n\nconst base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\nfunction byteAt(data: Uint8Array, index: number): number {\n const byte = data[index];\n return byte === undefined ? 0 : byte;\n}\n\nfunction symbolAt(index: number): string {\n return base64Alphabet.charAt(index);\n}\n\nexport function encodeAudioBase64(data: Uint8Array): string {\n let encoded = '';\n for (let index = 0; index < data.length; index += 3) {\n const second = data[index + 1];\n const third = data[index + 2];\n const triple =\n (byteAt(data, index) << 16) | (byteAt(data, index + 1) << 8) | byteAt(data, index + 2);\n encoded += symbolAt((triple >> 18) & 63);\n encoded += symbolAt((triple >> 12) & 63);\n encoded += second === undefined ? '=' : symbolAt((triple >> 6) & 63);\n encoded += third === undefined ? '=' : symbolAt(triple & 63);\n }\n return encoded;\n}\n\nfunction codeAt(value: string, index: number): number {\n const character = value[index];\n if (character === undefined) return -1;\n const code = base64Alphabet.indexOf(character);\n if (code < 0) throw new Error('Invalid base64 audio');\n return code;\n}\n\nfunction sextet(code: number): number {\n return code < 0 ? 0 : code;\n}\n\nexport function decodeAudioBase64(value: string): Uint8Array {\n const cleaned = value.replaceAll('=', '');\n if (cleaned.length % 4 === 1) throw new Error('Invalid base64 audio');\n const bytes = [];\n for (let index = 0; index < cleaned.length; index += 4) {\n const first = codeAt(cleaned, index);\n const second = codeAt(cleaned, index + 1);\n const third = codeAt(cleaned, index + 2);\n const fourth = codeAt(cleaned, index + 3);\n const triple =\n (sextet(first) << 18) | (sextet(second) << 12) | (sextet(third) << 6) | sextet(fourth);\n bytes.push((triple >> 16) & 0xff);\n if (third >= 0) bytes.push((triple >> 8) & 0xff);\n if (fourth >= 0) bytes.push(triple & 0xff);\n }\n return Uint8Array.from(bytes);\n}\n\nexport function convertAudio(chunk: AudioChunk, format: AudioFormat): AudioChunk {\n checkFormat(chunk.format);\n checkFormat(format);\n if (sameFormat(chunk.format, format)) {\n return { data: chunk.data.slice(), format, timestamp: chunk.timestamp };\n }\n if (compressed.has(chunk.format.encoding) || compressed.has(format.encoding)) {\n throw new Error(\n `Cannot convert ${chunk.format.encoding} to ${format.encoding} without a codec`\n );\n }\n const decoded = decode(chunk.data, chunk.format);\n const mixed = mix(decoded.channels, format.channels);\n const resampled = mixed.map((channel) =>\n resample(channel, decoded.sampleRate, format.sampleRate)\n );\n return {\n data: encode(resampled, format),\n format,\n timestamp: chunk.timestamp,\n };\n}\n\nexport async function* convertAudioSource(source: AudioSource, format: AudioFormat): AudioSource {\n for await (const chunk of source) yield convertAudio(chunk, format);\n}\n\nfunction sameFormat(left: AudioFormat, right: AudioFormat): boolean {\n return (\n left.encoding === right.encoding &&\n left.sampleRate === right.sampleRate &&\n left.channels === right.channels\n );\n}\n\nfunction checkFormat(format: AudioFormat): void {\n if (format.channels !== 1 && format.channels !== 2) {\n throw new Error(`Unsupported channel count ${String(format.channels)}`);\n }\n if (!Number.isFinite(format.sampleRate) || format.sampleRate <= 0) {\n throw new Error(`Unsupported sample rate ${String(format.sampleRate)}`);\n }\n}\n\ninterface Planar {\n channels: Float32Array[];\n sampleRate: number;\n}\n\nfunction decode(data: Uint8Array, format: AudioFormat): Planar {\n if (format.encoding === 'wav') return decodeWav(data);\n return { channels: decodePcm(data, format), sampleRate: format.sampleRate };\n}\n\nfunction decodePcm(data: Uint8Array, format: AudioFormat): Float32Array[] {\n const width = sampleWidth(format.encoding);\n const frames = Math.floor(data.byteLength / (width * format.channels));\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const channels = Array.from({ length: format.channels }, () => new Float32Array(frames));\n for (let frame = 0; frame < frames; frame++) {\n for (let channel = 0; channel < format.channels; channel++) {\n const offset = (frame * format.channels + channel) * width;\n channels[channel][frame] = readSample(view, offset, format.encoding);\n }\n }\n return channels;\n}\n\nfunction sampleWidth(encoding: AudioEncoding): number {\n if (encoding === 'pcm16') return 2;\n if (encoding === 'pcmf32') return 4;\n if (encoding === 'mulaw' || encoding === 'alaw') return 1;\n throw new Error(`Unsupported audio encoding ${encoding}`);\n}\n\nfunction readSample(view: DataView, offset: number, encoding: AudioEncoding): number {\n if (encoding === 'pcm16') return int16ToFloat(view.getInt16(offset, true));\n if (encoding === 'pcmf32') return finite(view.getFloat32(offset, true));\n if (encoding === 'mulaw') return int16ToFloat(mulawToLinear(view.getUint8(offset)));\n return int16ToFloat(alawToLinear(view.getUint8(offset)));\n}\n\nfunction encode(channels: Float32Array[], format: AudioFormat): Uint8Array {\n if (format.encoding === 'wav') {\n const pcm = encode(channels, { ...format, encoding: 'pcm16' });\n return wavBytes(pcm, format.sampleRate, format.channels);\n }\n const width = sampleWidth(format.encoding);\n const frames = channels[0]?.length ?? 0;\n const bytes = new Uint8Array(frames * format.channels * width);\n const view = new DataView(bytes.buffer);\n for (let frame = 0; frame < frames; frame++) {\n for (let channel = 0; channel < format.channels; channel++) {\n const offset = (frame * format.channels + channel) * width;\n writeSample(view, offset, format.encoding, channels[channel]?.[frame] ?? 0);\n }\n }\n return bytes;\n}\n\nfunction writeSample(\n view: DataView,\n offset: number,\n encoding: AudioEncoding,\n sample: number\n): void {\n const unit = finite(sample);\n if (encoding === 'pcm16') view.setInt16(offset, floatToInt16(unit), true);\n else if (encoding === 'pcmf32') view.setFloat32(offset, unit, true);\n else if (encoding === 'mulaw') view.setUint8(offset, linearToMulaw(floatToInt16(unit)));\n else view.setUint8(offset, linearToAlaw(floatToInt16(unit)));\n}\n\nfunction mix(channels: Float32Array[], target: 1 | 2): Float32Array[] {\n const length = channels[0]?.length ?? 0;\n if (channels.length === target) return channels;\n if (target === 1) {\n const mixed = new Float32Array(length);\n if (channels.length === 0) return [mixed];\n for (let index = 0; index < length; index++) {\n let sum = 0;\n for (const channel of channels) sum += channel[index] ?? 0;\n mixed[index] = sum / channels.length;\n }\n return [mixed];\n }\n const left = channels[0] ?? new Float32Array(length);\n return [left, channels[1] ?? left];\n}\n\nfunction resample(input: Float32Array, inRate: number, outRate: number): Float32Array {\n if (inRate === outRate || input.length === 0) return input;\n const outLength = Math.max(1, Math.round((input.length * outRate) / inRate));\n const output = new Float32Array(outLength);\n const scale = inRate / outRate;\n const last = input.length - 1;\n for (let index = 0; index < outLength; index++) {\n const position = index * scale;\n const cursor = Math.min(Math.floor(position), last);\n const next = Math.min(cursor + 1, last);\n const fraction = position - cursor;\n const left = input[cursor] ?? 0;\n const right = input[next] ?? 0;\n output[index] = left + (right - left) * fraction;\n }\n return output;\n}\n\nfunction finite(sample: number): number {\n return Number.isFinite(sample) ? sample : 0;\n}\n\nfunction floatToInt16(sample: number): number {\n const clamped = Math.max(-1, Math.min(1, sample));\n const scaled = clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff;\n return Math.max(-0x8000, Math.min(0x7fff, Math.round(scaled)));\n}\n\nfunction int16ToFloat(sample: number): number {\n return sample < 0 ? sample / 0x8000 : sample / 0x7fff;\n}\n\nfunction mulawToLinear(mulaw: number): number {\n const value = ~mulaw;\n const exponent = (value & 0x70) >> 4;\n let sample = ((value & 0x0f) << 3) + LAW_BIAS;\n sample <<= exponent;\n return (value & 0x80) !== 0 ? LAW_BIAS - sample : sample - LAW_BIAS;\n}\n\nfunction alawToLinear(alaw: number): number {\n const value = alaw ^ 0x55;\n const exponent = (value & 0x70) >> 4;\n const mantissa = value & 0x0f;\n const sample =\n exponent === 0\n ? (mantissa + mantissa + 1) << 3\n : (mantissa + mantissa + 1 + 32) << (exponent + 2);\n return (value & 0x80) !== 0 ? sample : -sample;\n}\n\nfunction buildLawTable(expand: (code: number) => number, mask: number): Uint8Array {\n const table = new Uint8Array(16384);\n table[8192] = mask;\n let cursor = 1;\n for (let code = 0; code < 127; code++) {\n const left = expand(code ^ mask);\n const right = expand((code + 1) ^ mask);\n const limit = (left + right + 4) >> 3;\n for (; cursor < limit; cursor += 1) {\n table[8192 - cursor] = (code ^ (mask ^ 0x80)) & 0xff;\n table[8192 + cursor] = (code ^ mask) & 0xff;\n }\n }\n for (; cursor < 8192; cursor++) {\n table[8192 - cursor] = (127 ^ (mask ^ 0x80)) & 0xff;\n table[8192 + cursor] = (127 ^ mask) & 0xff;\n }\n table[0] = table[1] ?? mask;\n return table;\n}\n\nconst mulawTable = buildLawTable(mulawToLinear, 0xff);\nconst alawTable = buildLawTable(alawToLinear, 0xd5);\n\nfunction lawCode(table: Uint8Array, pcm: number, fallback: number): number {\n return table[(pcm + 32768) >> 2] ?? fallback;\n}\n\nfunction linearToMulaw(pcm: number): number {\n return lawCode(mulawTable, pcm, 0xff);\n}\n\nfunction linearToAlaw(pcm: number): number {\n return lawCode(alawTable, pcm, 0xd5);\n}\n\nfunction decodeWav(data: Uint8Array): Planar {\n if (data.byteLength < 44) throw new Error('Invalid WAV audio');\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n if (text(data, 0, 4) !== 'RIFF' || text(data, 8, 4) !== 'WAVE')\n throw new Error('Invalid WAV audio');\n let cursor = 12;\n let format:\n | { audioFormat: number; channels: number; sampleRate: number; width: number }\n | undefined;\n let payload: Uint8Array | undefined;\n while (cursor + 8 <= data.byteLength) {\n const id = text(data, cursor, 4);\n const size = view.getUint32(cursor + 4, true);\n const start = cursor + 8;\n const end = Math.min(start + size, data.byteLength);\n if (id === 'fmt ' && end - start >= 16) {\n format = {\n audioFormat: view.getUint16(start, true),\n channels: view.getUint16(start + 2, true),\n sampleRate: view.getUint32(start + 4, true),\n width: view.getUint16(start + 14, true) / 8,\n };\n } else if (id === 'data') payload = data.subarray(start, end);\n cursor = start + size + (size % 2);\n }\n if (!format || !payload) throw new Error('Invalid WAV audio');\n if (format.audioFormat !== 1 || format.width !== 2) throw new Error('WAV audio must be PCM16');\n if (format.channels !== 1 && format.channels !== 2) throw new Error('Invalid WAV audio');\n return {\n channels: decodePcm(payload, {\n encoding: 'pcm16',\n sampleRate: format.sampleRate,\n channels: format.channels,\n }),\n sampleRate: format.sampleRate,\n };\n}\n\nfunction wavBytes(pcm: Uint8Array, sampleRate: number, channels: number): Uint8Array {\n const bytes = new Uint8Array(44 + pcm.byteLength);\n const view = new DataView(bytes.buffer);\n bytes.set(ascii('RIFF'), 0);\n view.setUint32(4, 36 + pcm.byteLength, true);\n bytes.set(ascii('WAVE'), 8);\n bytes.set(ascii('fmt '), 12);\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, channels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * channels * 2, true);\n view.setUint16(32, channels * 2, true);\n view.setUint16(34, 16, true);\n bytes.set(ascii('data'), 36);\n view.setUint32(40, pcm.byteLength, true);\n bytes.set(pcm, 44);\n return bytes;\n}\n\nfunction text(data: Uint8Array, offset: number, length: number): string {\n return String.fromCharCode(...data.subarray(offset, offset + length));\n}\n\nfunction ascii(value: string): Uint8Array {\n return Uint8Array.from(value, (character) => character.charCodeAt(0));\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,8 +1,8 @@
1
- import { blankUsage } from "../blank-usage.js";
2
1
  import {
3
2
  registerVoiceModel
4
3
  } from "../models.js";
5
4
  import { registerRealtimeApiProvider } from "../registry.js";
5
+ import { priceAudio } from "../usage.js";
6
6
  import { modelKey, pcm, shareApi, zeroCost } from "./shared.js";
7
7
  class FauxSession {
8
8
  model;
@@ -10,7 +10,6 @@ class FauxSession {
10
10
  #queue = [];
11
11
  #waiters = [];
12
12
  #closed = false;
13
- #usage = blankUsage();
14
13
  constructor(model, calls) {
15
14
  this.model = model;
16
15
  this.#calls = calls;
@@ -49,7 +48,13 @@ class FauxSession {
49
48
  this.#calls.push({ method: "close" });
50
49
  this.#closed = true;
51
50
  while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });
52
- return { usage: this.#usage };
51
+ return {
52
+ usage: priceAudio(this.model.audioCost, {
53
+ inputSeconds: 0,
54
+ outputSeconds: 0,
55
+ textMessages: 0
56
+ })
57
+ };
53
58
  }
54
59
  async *[Symbol.asyncIterator]() {
55
60
  while (true) {
@@ -91,7 +96,8 @@ function registerFauxRealtime(options = {}) {
91
96
  delivery: options.delivery ?? nativeDelivery,
92
97
  truncation: options.truncation ?? "client",
93
98
  liveToolUpdate: true,
94
- cost: zeroCost
99
+ cost: zeroCost,
100
+ audioCost: options.audioCost ?? {}
95
101
  };
96
102
  const state = { model, calls: [], events: options.events ?? [], pending: [] };
97
103
  states.set(modelKey(model), state);
@@ -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 { blankUsage } from '../blank-usage.ts';\nimport {\n type Delivery,\n type DeliverySupport,\n type RealtimeModel,\n registerVoiceModel,\n} from '../models.ts';\nimport type {\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n} from '../realtime.ts';\nimport { registerRealtimeApiProvider } from '../registry.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 }\n | {\n method: 'respond';\n callId: string;\n result: Parameters<RealtimeSession['respond']>[1];\n delivery?: Delivery;\n }\n | { method: 'interrupt'; heard?: { itemId: string; heardMs: number } }\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}\n\nexport interface FauxRealtime {\n model: RealtimeModel<string>;\n calls: FauxRealtimeCall[];\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 #queue: RealtimeEvent[] = [];\n readonly #waiters: Array<(result: IteratorResultEvent) => void> = [];\n #closed = false;\n readonly #usage: Usage = blankUsage();\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 push(event: RealtimeEvent): void {\n if (this.#closed) return;\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.#calls.push({ method: 'appendAudio', chunk });\n }\n\n appendVideoFrame(jpeg: Uint8Array): void {\n this.#calls.push({ method: 'appendVideoFrame', jpeg });\n }\n\n sendText(text: string): void {\n this.#calls.push({ method: 'sendText', text });\n }\n\n inject(text: string, delivery: Delivery): void {\n this.#calls.push({ method: 'inject', text, delivery });\n }\n\n respond(\n callId: string,\n result: Parameters<RealtimeSession['respond']>[1],\n delivery?: Delivery\n ): void {\n this.#calls.push({ method: 'respond', callId, result, delivery });\n }\n\n interrupt(heard?: { itemId: string; heardMs: number }): void {\n this.#calls.push({ method: 'interrupt', heard });\n }\n\n update(patch: Parameters<RealtimeSession['update']>[0]): void {\n this.#calls.push({ method: 'update', patch });\n }\n\n async close(): Promise<{ usage: Usage }> {\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.#usage };\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 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 };\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 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 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": "AAEA,SAAS,kBAAkB;AAC3B;AAAA,EAIE;AAAA,OACK;AAOP,SAAS,mCAAmC;AAC5C,SAAS,UAAU,KAAK,UAAU,gBAAgB;AA4ClD,MAAM,YAAuC;AAAA,EAClC;AAAA,EACA;AAAA,EACA,SAA0B,CAAC;AAAA,EAC3B,WAAyD,CAAC;AAAA,EACnE,UAAU;AAAA,EACD,SAAgB,WAAW;AAAA,EAEpC,YAAY,OAA8B,OAA2B;AACnE,SAAK,QAAQ;AACb,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,IAAI,SAAkB;AACpB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,KAAK,OAA4B;AAC/B,QAAI,KAAK,QAAS;AAClB,UAAM,SAAS,KAAK,SAAS,MAAM;AACnC,QAAI,OAAQ,QAAO,EAAE,OAAO,OAAO,MAAM,MAAM,CAAC;AAAA,QAC3C,MAAK,OAAO,KAAK,KAAK;AAAA,EAC7B;AAAA,EAEA,YAAY,OAAyB;AACnC,SAAK,OAAO,KAAK,EAAE,QAAQ,eAAe,MAAM,CAAC;AAAA,EACnD;AAAA,EAEA,iBAAiB,MAAwB;AACvC,SAAK,OAAO,KAAK,EAAE,QAAQ,oBAAoB,KAAK,CAAC;AAAA,EACvD;AAAA,EAEA,SAAS,MAAoB;AAC3B,SAAK,OAAO,KAAK,EAAE,QAAQ,YAAY,KAAK,CAAC;AAAA,EAC/C;AAAA,EAEA,OAAO,MAAc,UAA0B;AAC7C,SAAK,OAAO,KAAK,EAAE,QAAQ,UAAU,MAAM,SAAS,CAAC;AAAA,EACvD;AAAA,EAEA,QACE,QACA,QACA,UACM;AACN,SAAK,OAAO,KAAK,EAAE,QAAQ,WAAW,QAAQ,QAAQ,SAAS,CAAC;AAAA,EAClE;AAAA,EAEA,UAAU,OAAmD;AAC3D,SAAK,OAAO,KAAK,EAAE,QAAQ,aAAa,MAAM,CAAC;AAAA,EACjD;AAAA,EAEA,OAAO,OAAuD;AAC5D,SAAK,OAAO,KAAK,EAAE,QAAQ,UAAU,MAAM,CAAC;AAAA,EAC9C;AAAA,EAEA,MAAM,QAAmC;AACvC,SAAK,OAAO,KAAK,EAAE,QAAQ,QAAQ,CAAC;AACpC,SAAK,UAAU;AACf,WAAO,KAAK,SAAS,SAAS,EAAG,MAAK,SAAS,MAAM,IAAI,EAAE,MAAM,KAAK,CAAC;AACvE,WAAO,EAAE,OAAO,KAAK,OAAO;AAAA,EAC9B;AAAA,EAEA,QAAQ,OAAO,aAAa,IAAkC;AAC5D,WAAO,MAAM;AACX,YAAM,SAAS,KAAK,OAAO,MAAM;AACjC,UAAI,QAAQ;AACV,cAAM;AACN;AAAA,MACF;AACA,UAAI,KAAK,QAAS;AAClB,YAAM,OAAO,MAAM,IAAI,QAA6B,CAAC,YAAY,KAAK,SAAS,KAAK,OAAO,CAAC;AAC5F,UAAI,KAAK,QAAQ,KAAK,UAAU,OAAW;AAC3C,YAAM,KAAK;AAAA,IACb;AAAA,EACF;AACF;AAUA,MAAM,SAAS,oBAAI,IAA2B;AAC9C,MAAM,OAAO,oBAAI,IAAwB;AAEzC,MAAM,iBAAoD;AAAA,EACxD,WAAW;AAAA,EACX,WAAW;AAAA,EACX,QAAQ;AACV;AAEO,SAAS,qBAAqB,UAA+B,CAAC,GAAiB;AACpF,QAAM,QAA+B;AAAA,IACnC,IAAI,QAAQ,MAAM;AAAA,IAClB,MAAM,QAAQ,QAAQ;AAAA,IACtB,KAAK,QAAQ,OAAO;AAAA,IACpB,UAAU,QAAQ,YAAY;AAAA,IAC9B,SAAS;AAAA,IACT,MAAM;AAAA,IACN,YAAY,CAAC,WAAW;AAAA,IACxB,WAAW,QAAQ,aAAa;AAAA,IAChC,aAAa,IAAI,IAAK;AAAA,IACtB,cAAc,IAAI,IAAK;AAAA,IACvB,WAAW;AAAA,IACX,eAAe;AAAA,IACf,QAAQ,CAAC,EAAE,IAAI,QAAQ,MAAM,OAAO,CAAC;AAAA,IACrC,cAAc;AAAA,IACd,UAAU,QAAQ,YAAY;AAAA,IAC9B,YAAY,QAAQ,cAAc;AAAA,IAClC,gBAAgB;AAAA,IAChB,MAAM;AAAA,EACR;AACA,QAAM,QAAuB,EAAE,OAAO,OAAO,CAAC,GAAG,QAAQ,QAAQ,UAAU,CAAC,GAAG,SAAS,CAAC,EAAE;AAC3F,SAAO,IAAI,SAAS,KAAK,GAAG,KAAK;AACjC,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,QAAM,UAAU;AAAA,IACd;AAAA,IACA,MAAM;AAAA,IACN,MAAM,4BAA4B,EAAE,KAAK,MAAM,KAAK,SAAS,YAAY,CAAC;AAAA,IAC1E,MAAM,CAAC,GAAG,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,UAAU,MAAM,MAAM,QAAQ,MAAM,GAAG;AAAA,EAC1E;AACA,SAAO;AAAA,IACL;AAAA,IACA,OAAO,MAAM;AAAA,IACb,KAAK,OAAO;AACV,UAAI,MAAM,QAAS,OAAM,QAAQ,KAAK,KAAK;AAC3C,UAAI,CAAC,MAAM,WAAW,MAAM,QAAQ,OAAQ,OAAM,QAAQ,KAAK,KAAK;AAAA,IACtE;AAAA,IACA,SAAS,OAAO;AACd,UAAI,MAAM,UAAW,OAAM,MAAM,YAAY,MAAM;AACnD,UAAI,MAAM,SAAU,OAAM,MAAM,WAAW,MAAM;AACjD,UAAI,MAAM,WAAY,OAAM,MAAM,aAAa,MAAM;AAAA,IACvD;AAAA,IACA,aAAa;AACX,UAAI,OAAO,IAAI,SAAS,KAAK,CAAC,MAAM,MAAO,QAAO,OAAO,SAAS,KAAK,CAAC;AACxE,sBAAgB;AAChB,cAAQ;AAAA,IACV;AAAA,EACF;AACF;AAEA,eAAe,YACb,OACA,UACA,UAC0B;AAC1B,QAAM,QAAQ,OAAO,IAAI,SAAS,KAAK,CAAC;AACxC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,+BAA+B,MAAM,QAAQ,IAAI,MAAM,EAAE,EAAE;AACvF,QAAM,UAAU,IAAI,YAAY,MAAM,OAAO,MAAM,KAAK;AACxD,QAAM,UAAU;AAChB,aAAW,SAAS,MAAM,QAAS,SAAQ,KAAK,KAAK;AACrD,QAAM,UAAU,CAAC;AACjB,aAAW,SAAS,MAAM,OAAQ,SAAQ,KAAK,KAAK;AACpD,SAAO;AACT;",
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 type {\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n} from '../realtime.ts';\nimport { registerRealtimeApiProvider } from '../registry.ts';\nimport { priceAudio, 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 }\n | {\n method: 'respond';\n callId: string;\n result: Parameters<RealtimeSession['respond']>[1];\n delivery?: Delivery;\n }\n | { method: 'interrupt'; heard?: { itemId: string; heardMs: number } }\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 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 #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 push(event: RealtimeEvent): void {\n if (this.#closed) return;\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.#calls.push({ method: 'appendAudio', chunk });\n }\n\n appendVideoFrame(jpeg: Uint8Array): void {\n this.#calls.push({ method: 'appendVideoFrame', jpeg });\n }\n\n sendText(text: string): void {\n this.#calls.push({ method: 'sendText', text });\n }\n\n inject(text: string, delivery: Delivery): void {\n this.#calls.push({ method: 'inject', text, delivery });\n }\n\n respond(\n callId: string,\n result: Parameters<RealtimeSession['respond']>[1],\n delivery?: Delivery\n ): void {\n this.#calls.push({ method: 'respond', callId, result, delivery });\n }\n\n interrupt(heard?: { itemId: string; heardMs: number }): void {\n this.#calls.push({ method: 'interrupt', heard });\n }\n\n update(patch: Parameters<RealtimeSession['update']>[0]): void {\n this.#calls.push({ method: 'update', patch });\n }\n\n async close(): Promise<{ usage: Usage }> {\n this.#calls.push({ method: 'close' });\n this.#closed = true;\n while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });\n return {\n usage: priceAudio(this.model.audioCost, {\n inputSeconds: 0,\n outputSeconds: 0,\n textMessages: 0,\n }),\n };\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 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 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 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
  }
@@ -1,8 +1,8 @@
1
- import { blankUsage } from "../blank-usage.js";
2
1
  import { VoiceRequestError } from "../errors.js";
3
2
  import { registerVoiceModel } from "../models.js";
4
3
  import { registerSpeechApiProvider } from "../registry.js";
5
4
  import { SpeechEventStream } from "../speech.js";
5
+ import { priceAudio } from "../usage.js";
6
6
  import { encodeChars, errorMessage, modelKey, pcm, shareApi, zeroCost } from "./shared.js";
7
7
  const states = /* @__PURE__ */ new Map();
8
8
  const apis = /* @__PURE__ */ new Map();
@@ -18,7 +18,8 @@ function registerFauxSpeech(options = {}) {
18
18
  outputFormats: [pcm()],
19
19
  voices: [{ id: "faux", name: "Faux", languages: ["en"] }],
20
20
  cloning: false,
21
- cost: zeroCost
21
+ cost: zeroCost,
22
+ audioCost: options.audioCost ?? {}
22
23
  };
23
24
  const state = {
24
25
  model,
@@ -96,7 +97,13 @@ async function pumpSpeech(state, input, options, stream) {
96
97
  ...state.alignment ? { alignment: alignmentFor(start, slice.length) } : {}
97
98
  });
98
99
  }
99
- stream.push({ type: "done", usage: blankUsage() });
100
+ stream.push({
101
+ type: "done",
102
+ usage: priceAudio(state.model.audioCost, {
103
+ characters: text.length,
104
+ outputSeconds: text.length * 20 / 1e3
105
+ })
106
+ });
100
107
  } catch (error) {
101
108
  const reason = error instanceof VoiceRequestError ? error.reason : "error";
102
109
  stream.push({ type: "error", reason, error: errorMessage(error) });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/faux/speech.ts"],
4
- "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat, CharAlignment } from '../audio.ts';\nimport { blankUsage } from '../blank-usage.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport { registerVoiceModel, type SpeechModel } from '../models.ts';\nimport { registerSpeechApiProvider } from '../registry.ts';\nimport { SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { encodeChars, errorMessage, modelKey, pcm, shareApi, zeroCost } from './shared.ts';\n\nexport interface FauxSpeechOptions {\n api?: string;\n provider?: string;\n id?: string;\n name?: string;\n alignment?: boolean;\n chunkChars?: number;\n textStreaming?: boolean;\n}\n\nexport interface FauxSpeech {\n model: SpeechModel<string>;\n calls: SpeechInput[];\n setAlignment(alignment: boolean): void;\n unregister(): void;\n}\n\ninterface SpeechState {\n model: SpeechModel<string>;\n alignment: boolean;\n chunkChars: number;\n calls: SpeechInput[];\n}\n\nconst states = new Map<string, SpeechState>();\nconst apis = new Map<string, () => void>();\n\nexport function registerFauxSpeech(options: FauxSpeechOptions = {}): FauxSpeech {\n const model: SpeechModel<string> = {\n id: options.id ?? 'faux-speech',\n name: options.name ?? 'Faux speech',\n api: options.api ?? 'faux-speech',\n provider: options.provider ?? 'faux',\n baseUrl: 'http://localhost:0',\n type: 'speech',\n textStreaming: options.textStreaming ?? true,\n outputFormats: [pcm()],\n voices: [{ id: 'faux', name: 'Faux', languages: ['en'] }],\n cloning: false,\n cost: zeroCost,\n };\n const state: SpeechState = {\n model,\n alignment: options.alignment ?? false,\n chunkChars: Math.max(1, options.chunkChars ?? 4),\n calls: [],\n };\n states.set(modelKey(model), state);\n const unregisterModel = registerVoiceModel(model);\n const release = shareApi(\n apis,\n model.api,\n () => registerSpeechApiProvider({ api: model.api, speak: speakFaux }),\n () => [...states.values()].some((entry) => entry.model.api === model.api)\n );\n return {\n model,\n calls: state.calls,\n setAlignment(alignment) {\n state.alignment = alignment;\n },\n unregister() {\n if (states.get(modelKey(model)) === state) states.delete(modelKey(model));\n unregisterModel();\n release();\n },\n };\n}\n\nfunction speakFaux(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n const state = states.get(modelKey(model));\n if (!state) {\n stream.push({\n type: 'error',\n reason: 'error',\n error: `Unknown faux speech model ${model.provider}/${model.id}`,\n });\n return stream;\n }\n state.calls.push(input);\n void pumpSpeech(state, input, options, stream);\n return stream;\n}\n\nasync function readText(input: SpeechInput, signal: AbortSignal | undefined): Promise<string> {\n if (typeof input.text === 'string') return input.text;\n let text = '';\n for await (const part of input.text) {\n if (signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n text += part;\n }\n return text;\n}\n\nfunction alignmentFor(start: number, length: number): CharAlignment {\n const startMs = [];\n const durationMs = [];\n for (let offset = 0; offset < length; offset++) {\n startMs.push((start + offset) * 20);\n durationMs.push(20);\n }\n return { startMs, durationMs };\n}\n\nasync function pumpSpeech(\n state: SpeechState,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n try {\n if (options?.signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n const text = await readText(input, options?.signal);\n const format: AudioFormat | undefined = input.format ?? state.model.outputFormats[0];\n if (!format) throw new Error('No output format');\n stream.push({ type: 'start', format });\n for (let start = 0; start < text.length; start += state.chunkChars) {\n const slice = text.slice(start, start + state.chunkChars);\n stream.push({\n type: 'audio',\n chunk: { data: encodeChars(slice), format, timestamp: start * 20 },\n span: { itemId: state.model.id, start, end: start + slice.length },\n ...(state.alignment ? { alignment: alignmentFor(start, slice.length) } : {}),\n });\n }\n stream.push({ type: 'done', usage: blankUsage() });\n } catch (error) {\n const reason = error instanceof VoiceRequestError ? error.reason : 'error';\n stream.push({ type: 'error', reason, error: errorMessage(error) });\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat, CharAlignment } from '../audio.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport { registerVoiceModel, type SpeechModel } from '../models.ts';\nimport { registerSpeechApiProvider } from '../registry.ts';\nimport { SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { priceAudio, type SpeechAudioCost } from '../usage.ts';\nimport { encodeChars, errorMessage, modelKey, pcm, shareApi, zeroCost } from './shared.ts';\n\nexport interface FauxSpeechOptions {\n api?: string;\n provider?: string;\n id?: string;\n name?: string;\n alignment?: boolean;\n chunkChars?: number;\n textStreaming?: boolean;\n audioCost?: SpeechAudioCost;\n}\n\nexport interface FauxSpeech {\n model: SpeechModel<string>;\n calls: SpeechInput[];\n setAlignment(alignment: boolean): void;\n unregister(): void;\n}\n\ninterface SpeechState {\n model: SpeechModel<string>;\n alignment: boolean;\n chunkChars: number;\n calls: SpeechInput[];\n}\n\nconst states = new Map<string, SpeechState>();\nconst apis = new Map<string, () => void>();\n\nexport function registerFauxSpeech(options: FauxSpeechOptions = {}): FauxSpeech {\n const model: SpeechModel<string> = {\n id: options.id ?? 'faux-speech',\n name: options.name ?? 'Faux speech',\n api: options.api ?? 'faux-speech',\n provider: options.provider ?? 'faux',\n baseUrl: 'http://localhost:0',\n type: 'speech',\n textStreaming: options.textStreaming ?? true,\n outputFormats: [pcm()],\n voices: [{ id: 'faux', name: 'Faux', languages: ['en'] }],\n cloning: false,\n cost: zeroCost,\n audioCost: options.audioCost ?? {},\n };\n const state: SpeechState = {\n model,\n alignment: options.alignment ?? false,\n chunkChars: Math.max(1, options.chunkChars ?? 4),\n calls: [],\n };\n states.set(modelKey(model), state);\n const unregisterModel = registerVoiceModel(model);\n const release = shareApi(\n apis,\n model.api,\n () => registerSpeechApiProvider({ api: model.api, speak: speakFaux }),\n () => [...states.values()].some((entry) => entry.model.api === model.api)\n );\n return {\n model,\n calls: state.calls,\n setAlignment(alignment) {\n state.alignment = alignment;\n },\n unregister() {\n if (states.get(modelKey(model)) === state) states.delete(modelKey(model));\n unregisterModel();\n release();\n },\n };\n}\n\nfunction speakFaux(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n const state = states.get(modelKey(model));\n if (!state) {\n stream.push({\n type: 'error',\n reason: 'error',\n error: `Unknown faux speech model ${model.provider}/${model.id}`,\n });\n return stream;\n }\n state.calls.push(input);\n void pumpSpeech(state, input, options, stream);\n return stream;\n}\n\nasync function readText(input: SpeechInput, signal: AbortSignal | undefined): Promise<string> {\n if (typeof input.text === 'string') return input.text;\n let text = '';\n for await (const part of input.text) {\n if (signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n text += part;\n }\n return text;\n}\n\nfunction alignmentFor(start: number, length: number): CharAlignment {\n const startMs = [];\n const durationMs = [];\n for (let offset = 0; offset < length; offset++) {\n startMs.push((start + offset) * 20);\n durationMs.push(20);\n }\n return { startMs, durationMs };\n}\n\nasync function pumpSpeech(\n state: SpeechState,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n try {\n if (options?.signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n const text = await readText(input, options?.signal);\n const format: AudioFormat | undefined = input.format ?? state.model.outputFormats[0];\n if (!format) throw new Error('No output format');\n stream.push({ type: 'start', format });\n for (let start = 0; start < text.length; start += state.chunkChars) {\n const slice = text.slice(start, start + state.chunkChars);\n stream.push({\n type: 'audio',\n chunk: { data: encodeChars(slice), format, timestamp: start * 20 },\n span: { itemId: state.model.id, start, end: start + slice.length },\n ...(state.alignment ? { alignment: alignmentFor(start, slice.length) } : {}),\n });\n }\n stream.push({\n type: 'done',\n usage: priceAudio(state.model.audioCost, {\n characters: text.length,\n outputSeconds: (text.length * 20) / 1000,\n }),\n });\n } catch (error) {\n const reason = error instanceof VoiceRequestError ? error.reason : 'error';\n stream.push({ type: 'error', reason, error: errorMessage(error) });\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,10 @@
1
- import { blankUsage } from "../blank-usage.js";
2
1
  import { VoiceRequestError } from "../errors.js";
3
2
  import { registerVoiceModel } from "../models.js";
4
3
  import { registerTranscriptionApiProvider } from "../registry.js";
5
4
  import {
6
5
  TranscriptionEventStream
7
6
  } from "../transcription.js";
7
+ import { priceAudio } from "../usage.js";
8
8
  import { decodeBytes, errorMessage, modelKey, pcm, shareApi, zeroCost } from "./shared.js";
9
9
  const states = /* @__PURE__ */ new Map();
10
10
  const apis = /* @__PURE__ */ new Map();
@@ -26,7 +26,8 @@ function registerFauxTranscription(options = {}) {
26
26
  inputFormats: [pcm(16e3)],
27
27
  languages: ["en"],
28
28
  diarization: false,
29
- cost: zeroCost
29
+ cost: zeroCost,
30
+ audioCost: options.audioCost ?? {}
30
31
  };
31
32
  const state = { model, cues: options.cues, calls: [] };
32
33
  states.set(modelKey(model), state);
@@ -59,7 +60,7 @@ async function transcribeFaux(model, input, options) {
59
60
  const state = requireState(model);
60
61
  state.calls.push({ kind: "transcribe", input, chunks: [] });
61
62
  const text = "url" in input ? input.url : decodeBytes(input.data);
62
- return { text, usage: blankUsage() };
63
+ return { text, usage: priceAudio(state.model.audioCost, { inputSeconds: 0 }) };
63
64
  }
64
65
  function listenFaux(model, audio, options) {
65
66
  const stream = new TranscriptionEventStream();
@@ -92,7 +93,7 @@ function cuesAt(cues, at) {
92
93
  function endCues(cues, count) {
93
94
  return cues.filter((cue) => cue.at === "end" || typeof cue.at === "number" && cue.at > count).flatMap((cue) => cue.events);
94
95
  }
95
- function finishScript(pump, cues, text, at, count) {
96
+ function finishScript(pump, cues, text, at, count, rates) {
96
97
  if (pump.finished) return;
97
98
  if (cues) for (const event of endCues(cues, count)) pump.push(event);
98
99
  else {
@@ -102,7 +103,10 @@ function finishScript(pump, cues, text, at, count) {
102
103
  if (!pump.finished) {
103
104
  pump.push({
104
105
  type: "done",
105
- transcript: { text: pump.turnText ?? text, usage: blankUsage() }
106
+ transcript: {
107
+ text: pump.turnText ?? text,
108
+ usage: priceAudio(rates, { inputSeconds: at / 1e3 })
109
+ }
106
110
  });
107
111
  }
108
112
  }
@@ -133,7 +137,7 @@ async function pumpListen(state, audio, options, stream) {
133
137
  pump.push({ type: "error", reason: "aborted", error: "aborted" });
134
138
  return;
135
139
  }
136
- finishScript(pump, state.cues, text, at, count);
140
+ finishScript(pump, state.cues, text, at, count, state.model.audioCost);
137
141
  } catch (error) {
138
142
  pump.push({ type: "error", reason: "error", error: errorMessage(error) });
139
143
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/faux/transcription.ts"],
4
- "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioChunk, AudioSource } from '../audio.ts';\nimport { blankUsage } from '../blank-usage.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport { registerVoiceModel, type TranscriptionModel } from '../models.ts';\nimport { registerTranscriptionApiProvider } from '../registry.ts';\nimport {\n type AudioInput,\n type ListenOptions,\n type TranscriptionEvent,\n TranscriptionEventStream,\n} from '../transcription.ts';\nimport { decodeBytes, errorMessage, modelKey, pcm, shareApi, zeroCost } from './shared.ts';\n\nexport interface FauxTranscriptionCue {\n at: number | 'end';\n events: TranscriptionEvent[];\n}\n\nexport interface FauxTranscriptionOptions {\n api?: string;\n provider?: string;\n id?: string;\n name?: string;\n turnDetection?: TranscriptionModel<string>['turnDetection'];\n cues?: FauxTranscriptionCue[];\n}\n\nexport interface FauxTranscriptionCall {\n kind: 'transcribe' | 'listen';\n input?: AudioInput;\n chunks: AudioChunk[];\n}\n\nexport interface FauxTranscription {\n model: TranscriptionModel<string>;\n calls: FauxTranscriptionCall[];\n setCues(cues: FauxTranscriptionCue[] | undefined): void;\n unregister(): void;\n}\n\ninterface TranscriptionState {\n model: TranscriptionModel<string>;\n cues?: FauxTranscriptionCue[];\n calls: FauxTranscriptionCall[];\n}\n\nconst states = new Map<string, TranscriptionState>();\nconst apis = new Map<string, () => void>();\n\nfunction requireState(model: TranscriptionModel<string>): TranscriptionState {\n const state = states.get(modelKey(model));\n if (!state) throw new Error(`Unknown faux transcription model ${model.provider}/${model.id}`);\n return state;\n}\n\nexport function registerFauxTranscription(\n options: FauxTranscriptionOptions = {}\n): FauxTranscription {\n const model: TranscriptionModel<string> = {\n id: options.id ?? 'faux-transcription',\n name: options.name ?? 'Faux transcription',\n api: options.api ?? 'faux-transcription',\n provider: options.provider ?? 'faux',\n baseUrl: 'http://localhost:0',\n type: 'transcription',\n streaming: true,\n turnDetection: options.turnDetection ?? 'vad',\n inputFormats: [pcm(16000)],\n languages: ['en'],\n diarization: false,\n cost: zeroCost,\n };\n const state: TranscriptionState = { model, cues: options.cues, calls: [] };\n states.set(modelKey(model), state);\n const unregisterModel = registerVoiceModel(model);\n const release = shareApi(\n apis,\n model.api,\n () =>\n registerTranscriptionApiProvider({\n api: model.api,\n transcribe: transcribeFaux,\n listen: listenFaux,\n }),\n () => [...states.values()].some((entry) => entry.model.api === model.api)\n );\n return {\n model,\n calls: state.calls,\n setCues(cues) {\n state.cues = cues;\n },\n unregister() {\n if (states.get(modelKey(model)) === state) states.delete(modelKey(model));\n unregisterModel();\n release();\n },\n };\n}\n\nasync function transcribeFaux(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n) {\n if (options?.signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n const state = requireState(model);\n state.calls.push({ kind: 'transcribe', input, chunks: [] });\n const text = 'url' in input ? input.url : decodeBytes(input.data);\n return { text, usage: blankUsage() };\n}\n\nfunction listenFaux(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n): TranscriptionEventStream {\n const stream = new TranscriptionEventStream();\n let state: TranscriptionState;\n try {\n state = requireState(model);\n } catch (error) {\n stream.push({ type: 'error', reason: 'error', error: errorMessage(error) });\n return stream;\n }\n void pumpListen(state, audio, options, stream);\n return stream;\n}\n\ninterface Pump {\n stream: TranscriptionEventStream;\n finished: boolean;\n turnText?: string;\n push(event: TranscriptionEvent): void;\n}\n\nfunction createPump(stream: TranscriptionEventStream): Pump {\n const pump: Pump = {\n stream,\n finished: false,\n push(event) {\n if (pump.finished) return;\n if (event.type === 'turn_end') pump.turnText = event.text;\n stream.push(event);\n if (event.type === 'done' || event.type === 'error') pump.finished = true;\n },\n };\n return pump;\n}\n\nfunction cuesAt(cues: FauxTranscriptionCue[], at: number | 'end'): TranscriptionEvent[] {\n return cues.filter((cue) => cue.at === at).flatMap((cue) => cue.events);\n}\n\nfunction endCues(cues: FauxTranscriptionCue[], count: number): TranscriptionEvent[] {\n return cues\n .filter((cue) => cue.at === 'end' || (typeof cue.at === 'number' && cue.at > count))\n .flatMap((cue) => cue.events);\n}\n\nfunction finishScript(\n pump: Pump,\n cues: FauxTranscriptionCue[] | undefined,\n text: string,\n at: number,\n count: number\n) {\n if (pump.finished) return;\n if (cues) for (const event of endCues(cues, count)) pump.push(event);\n else {\n pump.push({ type: 'final', segmentId: '0', text });\n pump.push({ type: 'turn_end', text, at });\n }\n if (!pump.finished) {\n pump.push({\n type: 'done',\n transcript: { text: pump.turnText ?? text, usage: blankUsage() },\n });\n }\n}\n\nasync function pumpListen(\n state: TranscriptionState,\n audio: AudioSource,\n options: ListenOptions | undefined,\n stream: TranscriptionEventStream\n) {\n const pump = createPump(stream);\n const call: FauxTranscriptionCall = { kind: 'listen', chunks: [] };\n state.calls.push(call);\n let text = '';\n let at = 0;\n let count = 0;\n try {\n if (options?.signal?.aborted) {\n pump.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n for (const event of cuesAt(state.cues ?? [], 0)) pump.push(event);\n if (!pump.finished) {\n for await (const chunk of audio) {\n if (pump.finished || options?.signal?.aborted) break;\n call.chunks.push(chunk);\n count += 1;\n at = chunk.timestamp;\n text += decodeBytes(chunk.data);\n deliverChunk(pump, state.cues, chunk, count, text);\n }\n }\n if (options?.signal?.aborted && !pump.finished) {\n pump.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n finishScript(pump, state.cues, text, at, count);\n } catch (error) {\n pump.push({ type: 'error', reason: 'error', error: errorMessage(error) });\n }\n}\n\nfunction deliverChunk(\n pump: Pump,\n cues: FauxTranscriptionCue[] | undefined,\n chunk: AudioChunk,\n count: number,\n text: string\n) {\n if (cues) {\n for (const event of cuesAt(cues, count)) pump.push(event);\n return;\n }\n if (count === 1) pump.push({ type: 'speech_start', at: chunk.timestamp });\n pump.push({ type: 'partial', segmentId: '0', text });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioChunk, AudioSource } from '../audio.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport { registerVoiceModel, type TranscriptionModel } from '../models.ts';\nimport { registerTranscriptionApiProvider } from '../registry.ts';\nimport {\n type AudioInput,\n type ListenOptions,\n type TranscriptionEvent,\n TranscriptionEventStream,\n} from '../transcription.ts';\nimport { priceAudio, type TranscriptionAudioCost } from '../usage.ts';\nimport { decodeBytes, errorMessage, modelKey, pcm, shareApi, zeroCost } from './shared.ts';\n\nexport interface FauxTranscriptionCue {\n at: number | 'end';\n events: TranscriptionEvent[];\n}\n\nexport interface FauxTranscriptionOptions {\n api?: string;\n provider?: string;\n id?: string;\n name?: string;\n turnDetection?: TranscriptionModel<string>['turnDetection'];\n cues?: FauxTranscriptionCue[];\n audioCost?: TranscriptionAudioCost;\n}\n\nexport interface FauxTranscriptionCall {\n kind: 'transcribe' | 'listen';\n input?: AudioInput;\n chunks: AudioChunk[];\n}\n\nexport interface FauxTranscription {\n model: TranscriptionModel<string>;\n calls: FauxTranscriptionCall[];\n setCues(cues: FauxTranscriptionCue[] | undefined): void;\n unregister(): void;\n}\n\ninterface TranscriptionState {\n model: TranscriptionModel<string>;\n cues?: FauxTranscriptionCue[];\n calls: FauxTranscriptionCall[];\n}\n\nconst states = new Map<string, TranscriptionState>();\nconst apis = new Map<string, () => void>();\n\nfunction requireState(model: TranscriptionModel<string>): TranscriptionState {\n const state = states.get(modelKey(model));\n if (!state) throw new Error(`Unknown faux transcription model ${model.provider}/${model.id}`);\n return state;\n}\n\nexport function registerFauxTranscription(\n options: FauxTranscriptionOptions = {}\n): FauxTranscription {\n const model: TranscriptionModel<string> = {\n id: options.id ?? 'faux-transcription',\n name: options.name ?? 'Faux transcription',\n api: options.api ?? 'faux-transcription',\n provider: options.provider ?? 'faux',\n baseUrl: 'http://localhost:0',\n type: 'transcription',\n streaming: true,\n turnDetection: options.turnDetection ?? 'vad',\n inputFormats: [pcm(16000)],\n languages: ['en'],\n diarization: false,\n cost: zeroCost,\n audioCost: options.audioCost ?? {},\n };\n const state: TranscriptionState = { model, cues: options.cues, calls: [] };\n states.set(modelKey(model), state);\n const unregisterModel = registerVoiceModel(model);\n const release = shareApi(\n apis,\n model.api,\n () =>\n registerTranscriptionApiProvider({\n api: model.api,\n transcribe: transcribeFaux,\n listen: listenFaux,\n }),\n () => [...states.values()].some((entry) => entry.model.api === model.api)\n );\n return {\n model,\n calls: state.calls,\n setCues(cues) {\n state.cues = cues;\n },\n unregister() {\n if (states.get(modelKey(model)) === state) states.delete(modelKey(model));\n unregisterModel();\n release();\n },\n };\n}\n\nasync function transcribeFaux(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n) {\n if (options?.signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n const state = requireState(model);\n state.calls.push({ kind: 'transcribe', input, chunks: [] });\n const text = 'url' in input ? input.url : decodeBytes(input.data);\n return { text, usage: priceAudio(state.model.audioCost, { inputSeconds: 0 }) };\n}\n\nfunction listenFaux(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n): TranscriptionEventStream {\n const stream = new TranscriptionEventStream();\n let state: TranscriptionState;\n try {\n state = requireState(model);\n } catch (error) {\n stream.push({ type: 'error', reason: 'error', error: errorMessage(error) });\n return stream;\n }\n void pumpListen(state, audio, options, stream);\n return stream;\n}\n\ninterface Pump {\n stream: TranscriptionEventStream;\n finished: boolean;\n turnText?: string;\n push(event: TranscriptionEvent): void;\n}\n\nfunction createPump(stream: TranscriptionEventStream): Pump {\n const pump: Pump = {\n stream,\n finished: false,\n push(event) {\n if (pump.finished) return;\n if (event.type === 'turn_end') pump.turnText = event.text;\n stream.push(event);\n if (event.type === 'done' || event.type === 'error') pump.finished = true;\n },\n };\n return pump;\n}\n\nfunction cuesAt(cues: FauxTranscriptionCue[], at: number | 'end'): TranscriptionEvent[] {\n return cues.filter((cue) => cue.at === at).flatMap((cue) => cue.events);\n}\n\nfunction endCues(cues: FauxTranscriptionCue[], count: number): TranscriptionEvent[] {\n return cues\n .filter((cue) => cue.at === 'end' || (typeof cue.at === 'number' && cue.at > count))\n .flatMap((cue) => cue.events);\n}\n\nfunction finishScript(\n pump: Pump,\n cues: FauxTranscriptionCue[] | undefined,\n text: string,\n at: number,\n count: number,\n rates: TranscriptionAudioCost\n) {\n if (pump.finished) return;\n if (cues) for (const event of endCues(cues, count)) pump.push(event);\n else {\n pump.push({ type: 'final', segmentId: '0', text });\n pump.push({ type: 'turn_end', text, at });\n }\n if (!pump.finished) {\n pump.push({\n type: 'done',\n transcript: {\n text: pump.turnText ?? text,\n usage: priceAudio(rates, { inputSeconds: at / 1000 }),\n },\n });\n }\n}\n\nasync function pumpListen(\n state: TranscriptionState,\n audio: AudioSource,\n options: ListenOptions | undefined,\n stream: TranscriptionEventStream\n) {\n const pump = createPump(stream);\n const call: FauxTranscriptionCall = { kind: 'listen', chunks: [] };\n state.calls.push(call);\n let text = '';\n let at = 0;\n let count = 0;\n try {\n if (options?.signal?.aborted) {\n pump.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n for (const event of cuesAt(state.cues ?? [], 0)) pump.push(event);\n if (!pump.finished) {\n for await (const chunk of audio) {\n if (pump.finished || options?.signal?.aborted) break;\n call.chunks.push(chunk);\n count += 1;\n at = chunk.timestamp;\n text += decodeBytes(chunk.data);\n deliverChunk(pump, state.cues, chunk, count, text);\n }\n }\n if (options?.signal?.aborted && !pump.finished) {\n pump.push({ type: 'error', reason: 'aborted', error: 'aborted' });\n return;\n }\n finishScript(pump, state.cues, text, at, count, state.model.audioCost);\n } catch (error) {\n pump.push({ type: 'error', reason: 'error', error: errorMessage(error) });\n }\n}\n\nfunction deliverChunk(\n pump: Pump,\n cues: FauxTranscriptionCue[] | undefined,\n chunk: AudioChunk,\n count: number,\n text: string\n) {\n if (cues) {\n for (const event of cuesAt(cues, count)) pump.push(event);\n return;\n }\n if (count === 1) pump.push({ type: 'speech_start', at: chunk.timestamp });\n pump.push({ type: 'partial', segmentId: '0', text });\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import { resolveVoiceAuth } from "./auth.js";
2
2
  import { blankUsage } from "./blank-usage.js";
3
+ import {
4
+ convertAudio,
5
+ convertAudioSource,
6
+ decodeAudioBase64,
7
+ encodeAudioBase64
8
+ } from "./convert.js";
3
9
  import { VoiceRequestError } from "./errors.js";
4
10
  import {
5
11
  getVoiceModel,
@@ -17,15 +23,22 @@ import {
17
23
  } from "./registry.js";
18
24
  import { SpeechEventStream } from "./speech.js";
19
25
  import { TranscriptionEventStream } from "./transcription.js";
26
+ import { addAudioCounters, priceAudio } from "./usage.js";
20
27
  export {
21
28
  SpeechEventStream,
22
29
  TranscriptionEventStream,
23
30
  VoiceRequestError,
31
+ addAudioCounters,
24
32
  blankUsage,
25
33
  connectRealtime,
34
+ convertAudio,
35
+ convertAudioSource,
36
+ decodeAudioBase64,
37
+ encodeAudioBase64,
26
38
  getVoiceModel,
27
39
  listVoiceModels,
28
40
  listen,
41
+ priceAudio,
29
42
  registerRealtimeApiProvider,
30
43
  registerSpeechApiProvider,
31
44
  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 { 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';\n"],
5
- "mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B,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;",
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;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/models.ts"],
4
- "sourcesContent": ["import type { BaseModel } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from './audio.ts';\n\nexport type KnownTranscriptionApi =\n | 'openai-transcriptions'\n | 'openai-realtime-transcription'\n | 'xai-stt'\n | 'elevenlabs-scribe'\n | 'mistral-transcriptions'\n | 'fish-asr'\n | 'cloudflare-ai-run';\n\nexport type TranscriptionApi = KnownTranscriptionApi | (string & {});\n\nexport type KnownSpeechApi =\n | 'openai-speech'\n | 'xai-tts'\n | 'elevenlabs-tts'\n | 'mistral-speech'\n | 'fish-tts'\n | 'google-tts'\n | 'cloudflare-ai-run'\n | 'wavespeed';\n\nexport type SpeechApi = KnownSpeechApi | (string & {});\n\nexport type KnownRealtimeApi =\n | 'openai-realtime'\n | 'xai-realtime'\n | 'gemini-live'\n | 'elevenlabs-agents'\n | 'fish-agents';\n\nexport type RealtimeApi = KnownRealtimeApi | (string & {});\n\nexport type Delivery = 'interrupt' | 'when_idle' | 'silent';\n\nexport type DeliverySupport = 'native' | 'approximate' | 'unsupported';\n\nexport interface VoiceInfo {\n id: string;\n name: string;\n languages?: string[];\n}\n\nexport interface TranscriptionModel<TApi extends TranscriptionApi>\n extends Omit<BaseModel<TApi>, 'input'> {\n type: 'transcription';\n streaming: boolean;\n turnDetection: 'none' | 'vad' | 'semantic';\n inputFormats: AudioFormat[];\n languages?: string[];\n diarization?: boolean;\n}\n\nexport interface SpeechModel<TApi extends SpeechApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'speech';\n textStreaming: boolean;\n outputFormats: AudioFormat[];\n voices?: VoiceInfo[];\n cloning?: boolean;\n}\n\nexport interface RealtimeModel<TApi extends RealtimeApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'realtime';\n transports: ('websocket' | 'webrtc')[];\n toolCalls: 'blocking' | 'async';\n inputFormat: AudioFormat;\n outputFormat: AudioFormat;\n video?: { maxFps: number };\n maxSessionMs?: number;\n resumable: boolean;\n ephemeralAuth: boolean;\n voices: VoiceInfo[];\n cascade?: { stt: string; llm: string; tts: string };\n provisioning: 'none' | 'agent';\n delivery: Record<Delivery, DeliverySupport>;\n truncation: 'client' | 'server' | 'none';\n liveToolUpdate: boolean;\n toolTimeoutMs?: { min: number; max: number };\n}\n\ndeclare module '@earendil-works/pi-ai' {\n interface ModelTypeMap {\n transcription: TranscriptionModel<TranscriptionApi>;\n speech: SpeechModel<SpeechApi>;\n realtime: RealtimeModel<RealtimeApi>;\n }\n}\n\nexport type VoiceModel =\n | TranscriptionModel<TranscriptionApi>\n | SpeechModel<SpeechApi>\n | RealtimeModel<RealtimeApi>;\n\nexport type VoiceModelType = VoiceModel['type'];\n\nconst catalog = new Map<string, VoiceModel>();\n\nfunction catalogKey(type: string, provider: string, id: string): string {\n return `${type}\\0${provider}\\0${id}`;\n}\n\nexport function registerVoiceModel(model: VoiceModel): () => void {\n const key = catalogKey(model.type, model.provider, model.id);\n catalog.set(key, model);\n return () => {\n if (catalog.get(key) === model) catalog.delete(key);\n };\n}\n\nexport function getVoiceModel<TType extends VoiceModelType>(\n type: TType,\n provider: string,\n id: string\n): Extract<VoiceModel, { type: TType }> | undefined {\n return catalog.get(catalogKey(type, provider, id)) as\n | Extract<VoiceModel, { type: TType }>\n | undefined;\n}\n\nexport function listVoiceModels<TType extends VoiceModelType>(\n type: TType,\n provider?: string\n): Extract<VoiceModel, { type: TType }>[] {\n const found: Extract<VoiceModel, { type: TType }>[] = [];\n for (const model of catalog.values()) {\n if (model.type !== type) continue;\n if (provider !== undefined && model.provider !== provider) continue;\n found.push(model as Extract<VoiceModel, { type: TType }>);\n }\n return found;\n}\n"],
5
- "mappings": "AAiGA,MAAM,UAAU,oBAAI,IAAwB;AAE5C,SAAS,WAAW,MAAc,UAAkB,IAAoB;AACtE,SAAO,GAAG,IAAI,KAAK,QAAQ,KAAK,EAAE;AACpC;AAEO,SAAS,mBAAmB,OAA+B;AAChE,QAAM,MAAM,WAAW,MAAM,MAAM,MAAM,UAAU,MAAM,EAAE;AAC3D,UAAQ,IAAI,KAAK,KAAK;AACtB,SAAO,MAAM;AACX,QAAI,QAAQ,IAAI,GAAG,MAAM,MAAO,SAAQ,OAAO,GAAG;AAAA,EACpD;AACF;AAEO,SAAS,cACd,MACA,UACA,IACkD;AAClD,SAAO,QAAQ,IAAI,WAAW,MAAM,UAAU,EAAE,CAAC;AAGnD;AAEO,SAAS,gBACd,MACA,UACwC;AACxC,QAAM,QAAgD,CAAC;AACvD,aAAW,SAAS,QAAQ,OAAO,GAAG;AACpC,QAAI,MAAM,SAAS,KAAM;AACzB,QAAI,aAAa,UAAa,MAAM,aAAa,SAAU;AAC3D,UAAM,KAAK,KAA6C;AAAA,EAC1D;AACA,SAAO;AACT;",
4
+ "sourcesContent": ["import type { BaseModel } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from './audio.ts';\nimport type { RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost } from './usage.ts';\n\nexport type KnownTranscriptionApi =\n | 'openai-transcriptions'\n | 'openai-realtime-transcription'\n | 'xai-stt'\n | 'elevenlabs-scribe'\n | 'mistral-transcriptions'\n | 'fish-asr'\n | 'cloudflare-ai-run';\n\nexport type TranscriptionApi = KnownTranscriptionApi | (string & {});\n\nexport type KnownSpeechApi =\n | 'openai-speech'\n | 'xai-tts'\n | 'elevenlabs-tts'\n | 'mistral-speech'\n | 'fish-tts'\n | 'google-tts'\n | 'cloudflare-ai-run'\n | 'wavespeed';\n\nexport type SpeechApi = KnownSpeechApi | (string & {});\n\nexport type KnownRealtimeApi =\n | 'openai-realtime'\n | 'xai-realtime'\n | 'gemini-live'\n | 'elevenlabs-agents'\n | 'fish-agents';\n\nexport type RealtimeApi = KnownRealtimeApi | (string & {});\n\nexport type Delivery = 'interrupt' | 'when_idle' | 'silent';\n\nexport type DeliverySupport = 'native' | 'approximate' | 'unsupported';\n\nexport interface VoiceInfo {\n id: string;\n name: string;\n languages?: string[];\n}\n\nexport interface TranscriptionModel<TApi extends TranscriptionApi>\n extends Omit<BaseModel<TApi>, 'input'> {\n type: 'transcription';\n streaming: boolean;\n turnDetection: 'none' | 'vad' | 'semantic';\n inputFormats: AudioFormat[];\n languages?: string[];\n diarization?: boolean;\n audioCost: TranscriptionAudioCost;\n}\n\nexport interface SpeechModel<TApi extends SpeechApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'speech';\n textStreaming: boolean;\n outputFormats: AudioFormat[];\n voices?: VoiceInfo[];\n cloning?: boolean;\n audioCost: SpeechAudioCost;\n}\n\nexport interface RealtimeModel<TApi extends RealtimeApi> extends Omit<BaseModel<TApi>, 'input'> {\n type: 'realtime';\n transports: ('websocket' | 'webrtc')[];\n toolCalls: 'blocking' | 'async';\n inputFormat: AudioFormat;\n outputFormat: AudioFormat;\n video?: { maxFps: number };\n maxSessionMs?: number;\n resumable: boolean;\n ephemeralAuth: boolean;\n voices: VoiceInfo[];\n cascade?: { stt: string; llm: string; tts: string };\n provisioning: 'none' | 'agent';\n delivery: Record<Delivery, DeliverySupport>;\n truncation: 'client' | 'server' | 'none';\n liveToolUpdate: boolean;\n toolTimeoutMs?: { min: number; max: number };\n audioCost: RealtimeAudioCost;\n}\n\ndeclare module '@earendil-works/pi-ai' {\n interface ModelTypeMap {\n transcription: TranscriptionModel<TranscriptionApi>;\n speech: SpeechModel<SpeechApi>;\n realtime: RealtimeModel<RealtimeApi>;\n }\n}\n\nexport type VoiceModel =\n | TranscriptionModel<TranscriptionApi>\n | SpeechModel<SpeechApi>\n | RealtimeModel<RealtimeApi>;\n\nexport type VoiceModelType = VoiceModel['type'];\n\nconst catalog = new Map<string, VoiceModel>();\n\nfunction catalogKey(type: string, provider: string, id: string): string {\n return `${type}\\0${provider}\\0${id}`;\n}\n\nexport function registerVoiceModel(model: VoiceModel): () => void {\n const key = catalogKey(model.type, model.provider, model.id);\n catalog.set(key, model);\n return () => {\n if (catalog.get(key) === model) catalog.delete(key);\n };\n}\n\nexport function getVoiceModel<TType extends VoiceModelType>(\n type: TType,\n provider: string,\n id: string\n): Extract<VoiceModel, { type: TType }> | undefined {\n return catalog.get(catalogKey(type, provider, id)) as\n | Extract<VoiceModel, { type: TType }>\n | undefined;\n}\n\nexport function listVoiceModels<TType extends VoiceModelType>(\n type: TType,\n provider?: string\n): Extract<VoiceModel, { type: TType }>[] {\n const found: Extract<VoiceModel, { type: TType }>[] = [];\n for (const model of catalog.values()) {\n if (model.type !== type) continue;\n if (provider !== undefined && model.provider !== provider) continue;\n found.push(model as Extract<VoiceModel, { type: TType }>);\n }\n return found;\n}\n"],
5
+ "mappings": "AAqGA,MAAM,UAAU,oBAAI,IAAwB;AAE5C,SAAS,WAAW,MAAc,UAAkB,IAAoB;AACtE,SAAO,GAAG,IAAI,KAAK,QAAQ,KAAK,EAAE;AACpC;AAEO,SAAS,mBAAmB,OAA+B;AAChE,QAAM,MAAM,WAAW,MAAM,MAAM,MAAM,UAAU,MAAM,EAAE;AAC3D,UAAQ,IAAI,KAAK,KAAK;AACtB,SAAO,MAAM;AACX,QAAI,QAAQ,IAAI,GAAG,MAAM,MAAO,SAAQ,OAAO,GAAG;AAAA,EACpD;AACF;AAEO,SAAS,cACd,MACA,UACA,IACkD;AAClD,SAAO,QAAQ,IAAI,WAAW,MAAM,UAAU,EAAE,CAAC;AAGnD;AAEO,SAAS,gBACd,MACA,UACwC;AACxC,QAAM,QAAgD,CAAC;AACvD,aAAW,SAAS,QAAQ,OAAO,GAAG;AACpC,QAAI,MAAM,SAAS,KAAM;AACzB,QAAI,aAAa,UAAa,MAAM,aAAa,SAAU;AAC3D,UAAM,KAAK,KAA6C;AAAA,EAC1D;AACA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,5 @@
1
+ import type { AudioChunk, AudioFormat, AudioSource } from './audio.ts';
2
+ export declare function encodeAudioBase64(data: Uint8Array): string;
3
+ export declare function decodeAudioBase64(value: string): Uint8Array;
4
+ export declare function convertAudio(chunk: AudioChunk, format: AudioFormat): AudioChunk;
5
+ export declare function convertAudioSource(source: AudioSource, format: AudioFormat): AudioSource;
@@ -1,6 +1,7 @@
1
1
  import type { AudioChunk } from '../audio.ts';
2
2
  import { type Delivery, type DeliverySupport, type RealtimeModel } from '../models.ts';
3
3
  import type { RealtimeEvent, RealtimeSession } from '../realtime.ts';
4
+ import { type RealtimeAudioCost } from '../usage.ts';
4
5
  export type FauxRealtimeCall = {
5
6
  method: 'appendAudio';
6
7
  chunk: AudioChunk;
@@ -42,6 +43,7 @@ export interface FauxRealtimeOptions extends FauxRealtimeFlags {
42
43
  id?: string;
43
44
  name?: string;
44
45
  events?: RealtimeEvent[];
46
+ audioCost?: RealtimeAudioCost;
45
47
  }
46
48
  export interface FauxRealtime {
47
49
  model: RealtimeModel<string>;
@@ -1,5 +1,6 @@
1
1
  import { type SpeechModel } from '../models.ts';
2
2
  import { type SpeechInput } from '../speech.ts';
3
+ import { type SpeechAudioCost } from '../usage.ts';
3
4
  export interface FauxSpeechOptions {
4
5
  api?: string;
5
6
  provider?: string;
@@ -8,6 +9,7 @@ export interface FauxSpeechOptions {
8
9
  alignment?: boolean;
9
10
  chunkChars?: number;
10
11
  textStreaming?: boolean;
12
+ audioCost?: SpeechAudioCost;
11
13
  }
12
14
  export interface FauxSpeech {
13
15
  model: SpeechModel<string>;
@@ -1,6 +1,7 @@
1
1
  import type { AudioChunk } from '../audio.ts';
2
2
  import { type TranscriptionModel } from '../models.ts';
3
3
  import { type AudioInput, type TranscriptionEvent } from '../transcription.ts';
4
+ import { type TranscriptionAudioCost } from '../usage.ts';
4
5
  export interface FauxTranscriptionCue {
5
6
  at: number | 'end';
6
7
  events: TranscriptionEvent[];
@@ -12,6 +13,7 @@ export interface FauxTranscriptionOptions {
12
13
  name?: string;
13
14
  turnDetection?: TranscriptionModel<string>['turnDetection'];
14
15
  cues?: FauxTranscriptionCue[];
16
+ audioCost?: TranscriptionAudioCost;
15
17
  }
16
18
  export interface FauxTranscriptionCall {
17
19
  kind: 'transcribe' | 'listen';
@@ -1,6 +1,7 @@
1
1
  export type { AudioChunk, AudioEncoding, AudioFormat, AudioSink, AudioSource, AudioTag, CharAlignment, TextSpan, } from './audio.ts';
2
2
  export { resolveVoiceAuth } from './auth.ts';
3
3
  export { blankUsage } from './blank-usage.ts';
4
+ export { convertAudio, convertAudioSource, decodeAudioBase64, encodeAudioBase64, } from './convert.ts';
4
5
  export { VoiceRequestError } from './errors.ts';
5
6
  export type { Delivery, DeliverySupport, KnownRealtimeApi, KnownSpeechApi, KnownTranscriptionApi, RealtimeApi, RealtimeModel, SpeechApi, SpeechModel, TranscriptionApi, TranscriptionModel, VoiceInfo, VoiceModel, VoiceModelType, } from './models.ts';
6
7
  export { getVoiceModel, listVoiceModels, registerVoiceModel, } from './models.ts';
@@ -10,3 +11,5 @@ export type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';
10
11
  export { SpeechEventStream } from './speech.ts';
11
12
  export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, TranscriptionEvent, TranscriptSegment, } from './transcription.ts';
12
13
  export { TranscriptionEventStream } from './transcription.ts';
14
+ export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
15
+ export { addAudioCounters, priceAudio } from './usage.ts';
@@ -1,5 +1,6 @@
1
1
  import type { BaseModel } from '@earendil-works/pi-ai';
2
2
  import type { AudioFormat } from './audio.ts';
3
+ import type { RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost } from './usage.ts';
3
4
  export type KnownTranscriptionApi = 'openai-transcriptions' | 'openai-realtime-transcription' | 'xai-stt' | 'elevenlabs-scribe' | 'mistral-transcriptions' | 'fish-asr' | 'cloudflare-ai-run';
4
5
  export type TranscriptionApi = KnownTranscriptionApi | (string & {});
5
6
  export type KnownSpeechApi = 'openai-speech' | 'xai-tts' | 'elevenlabs-tts' | 'mistral-speech' | 'fish-tts' | 'google-tts' | 'cloudflare-ai-run' | 'wavespeed';
@@ -20,6 +21,7 @@ export interface TranscriptionModel<TApi extends TranscriptionApi> extends Omit<
20
21
  inputFormats: AudioFormat[];
21
22
  languages?: string[];
22
23
  diarization?: boolean;
24
+ audioCost: TranscriptionAudioCost;
23
25
  }
24
26
  export interface SpeechModel<TApi extends SpeechApi> extends Omit<BaseModel<TApi>, 'input'> {
25
27
  type: 'speech';
@@ -27,6 +29,7 @@ export interface SpeechModel<TApi extends SpeechApi> extends Omit<BaseModel<TApi
27
29
  outputFormats: AudioFormat[];
28
30
  voices?: VoiceInfo[];
29
31
  cloning?: boolean;
32
+ audioCost: SpeechAudioCost;
30
33
  }
31
34
  export interface RealtimeModel<TApi extends RealtimeApi> extends Omit<BaseModel<TApi>, 'input'> {
32
35
  type: 'realtime';
@@ -54,6 +57,7 @@ export interface RealtimeModel<TApi extends RealtimeApi> extends Omit<BaseModel<
54
57
  min: number;
55
58
  max: number;
56
59
  };
60
+ audioCost: RealtimeAudioCost;
57
61
  }
58
62
  declare module '@earendil-works/pi-ai' {
59
63
  interface ModelTypeMap {
@@ -0,0 +1,28 @@
1
+ import type { Usage } from '@earendil-works/pi-ai';
2
+ export interface AudioCounters {
3
+ inputSeconds?: number;
4
+ outputSeconds?: number;
5
+ characters?: number;
6
+ textMessages?: number;
7
+ }
8
+ export interface TranscriptionAudioCost {
9
+ perInputMinute?: number;
10
+ }
11
+ export interface SpeechAudioCost {
12
+ perThousandChars?: number;
13
+ perOutputMinute?: number;
14
+ }
15
+ export interface RealtimeAudioCost {
16
+ perInputMinute?: number;
17
+ perOutputMinute?: number;
18
+ perTextMessage?: number;
19
+ }
20
+ export type AudioCost = TranscriptionAudioCost & SpeechAudioCost & RealtimeAudioCost;
21
+ declare module '@earendil-works/pi-ai' {
22
+ interface Usage {
23
+ audio?: AudioCounters;
24
+ }
25
+ }
26
+ export type AudioUsage = Usage;
27
+ export declare function priceAudio(rates: AudioCost, meters: AudioCounters): AudioUsage;
28
+ export declare function addAudioCounters(total: AudioUsage, usage: AudioUsage): void;
@@ -0,0 +1,30 @@
1
+ import type { AudioFormat } from '../audio.ts';
2
+ export declare const AUDIO_CONVERT_PROCESSOR = "pi-audio-convert";
3
+ export interface AudioConvertProcessorOptions {
4
+ processorOptions?: {
5
+ format?: AudioFormat;
6
+ sampleRate?: number;
7
+ };
8
+ }
9
+ interface WorkletScope {
10
+ AudioWorkletProcessor?: new (options?: AudioConvertProcessorOptions) => {
11
+ port?: {
12
+ postMessage(message: unknown): void;
13
+ };
14
+ };
15
+ registerProcessor?: (name: string, processor: new (options?: AudioConvertProcessorOptions) => object) => void;
16
+ }
17
+ declare const WorkletBase: new (options?: AudioConvertProcessorOptions) => {
18
+ port?: {
19
+ postMessage(message: unknown): void;
20
+ };
21
+ };
22
+ export declare class AudioConvertProcessor extends WorkletBase {
23
+ #private;
24
+ constructor(options?: AudioConvertProcessorOptions);
25
+ process(inputs: Array<ArrayLike<Float32Array | undefined> | undefined>): boolean;
26
+ take(): Uint8Array[];
27
+ }
28
+ export declare function registerAudioConvertWorklet(target?: WorkletScope): void;
29
+ export declare function audioConvertWorkletUrl(): string;
30
+ export {};
package/dist/usage.js ADDED
@@ -0,0 +1,33 @@
1
+ const audioKeys = ["inputSeconds", "outputSeconds", "characters", "textMessages"];
2
+ function priceAudio(rates, meters) {
3
+ const audio = {};
4
+ for (const key of audioKeys) {
5
+ const value = meters[key];
6
+ if (value !== void 0) audio[key] = value;
7
+ }
8
+ const input = (meters.inputSeconds ?? 0) / 60 * (rates.perInputMinute ?? 0) + (meters.textMessages ?? 0) * (rates.perTextMessage ?? 0);
9
+ const output = (meters.outputSeconds ?? 0) / 60 * (rates.perOutputMinute ?? 0) + (meters.characters ?? 0) / 1e3 * (rates.perThousandChars ?? 0);
10
+ return {
11
+ input: 0,
12
+ output: 0,
13
+ cacheRead: 0,
14
+ cacheWrite: 0,
15
+ totalTokens: 0,
16
+ audio,
17
+ cost: { input, output, cacheRead: 0, cacheWrite: 0, total: input + output }
18
+ };
19
+ }
20
+ function addAudioCounters(total, usage) {
21
+ if (usage.audio === void 0) return;
22
+ const audio = total.audio ?? {};
23
+ total.audio = audio;
24
+ for (const key of audioKeys) {
25
+ const value = usage.audio[key];
26
+ if (value !== void 0) audio[key] = (audio[key] ?? 0) + value;
27
+ }
28
+ }
29
+ export {
30
+ addAudioCounters,
31
+ priceAudio
32
+ };
33
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/usage.ts"],
4
+ "sourcesContent": ["import type { Usage } from '@earendil-works/pi-ai';\n\nexport interface AudioCounters {\n inputSeconds?: number;\n outputSeconds?: number;\n characters?: number;\n textMessages?: number;\n}\n\nexport interface TranscriptionAudioCost {\n perInputMinute?: number;\n}\n\nexport interface SpeechAudioCost {\n perThousandChars?: number;\n perOutputMinute?: number;\n}\n\nexport interface RealtimeAudioCost {\n perInputMinute?: number;\n perOutputMinute?: number;\n perTextMessage?: number;\n}\n\nexport type AudioCost = TranscriptionAudioCost & SpeechAudioCost & RealtimeAudioCost;\n\ndeclare module '@earendil-works/pi-ai' {\n interface Usage {\n audio?: AudioCounters;\n }\n}\n\nexport type AudioUsage = Usage;\n\nconst audioKeys = ['inputSeconds', 'outputSeconds', 'characters', 'textMessages'] as const;\n\nexport function priceAudio(rates: AudioCost, meters: AudioCounters): AudioUsage {\n const audio: AudioCounters = {};\n for (const key of audioKeys) {\n const value = meters[key];\n if (value !== undefined) audio[key] = value;\n }\n const input =\n ((meters.inputSeconds ?? 0) / 60) * (rates.perInputMinute ?? 0) +\n (meters.textMessages ?? 0) * (rates.perTextMessage ?? 0);\n const output =\n ((meters.outputSeconds ?? 0) / 60) * (rates.perOutputMinute ?? 0) +\n ((meters.characters ?? 0) / 1000) * (rates.perThousandChars ?? 0);\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n audio,\n cost: { input, output, cacheRead: 0, cacheWrite: 0, total: input + output },\n };\n}\n\nexport function addAudioCounters(total: AudioUsage, usage: AudioUsage): void {\n if (usage.audio === undefined) return;\n const audio = total.audio ?? {};\n total.audio = audio;\n for (const key of audioKeys) {\n const value = usage.audio[key];\n if (value !== undefined) audio[key] = (audio[key] ?? 0) + value;\n }\n}\n"],
5
+ "mappings": "AAkCA,MAAM,YAAY,CAAC,gBAAgB,iBAAiB,cAAc,cAAc;AAEzE,SAAS,WAAW,OAAkB,QAAmC;AAC9E,QAAM,QAAuB,CAAC;AAC9B,aAAW,OAAO,WAAW;AAC3B,UAAM,QAAQ,OAAO,GAAG;AACxB,QAAI,UAAU,OAAW,OAAM,GAAG,IAAI;AAAA,EACxC;AACA,QAAM,SACF,OAAO,gBAAgB,KAAK,MAAO,MAAM,kBAAkB,MAC5D,OAAO,gBAAgB,MAAM,MAAM,kBAAkB;AACxD,QAAM,UACF,OAAO,iBAAiB,KAAK,MAAO,MAAM,mBAAmB,MAC7D,OAAO,cAAc,KAAK,OAAS,MAAM,oBAAoB;AACjE,SAAO;AAAA,IACL,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,aAAa;AAAA,IACb;AAAA,IACA,MAAM,EAAE,OAAO,QAAQ,WAAW,GAAG,YAAY,GAAG,OAAO,QAAQ,OAAO;AAAA,EAC5E;AACF;AAEO,SAAS,iBAAiB,OAAmB,OAAyB;AAC3E,MAAI,MAAM,UAAU,OAAW;AAC/B,QAAM,QAAQ,MAAM,SAAS,CAAC;AAC9B,QAAM,QAAQ;AACd,aAAW,OAAO,WAAW;AAC3B,UAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAW,OAAM,GAAG,KAAK,MAAM,GAAG,KAAK,KAAK;AAAA,EAC5D;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,80 @@
1
+ import { convertAudio } from "../convert.js";
2
+ const AUDIO_CONVERT_PROCESSOR = "pi-audio-convert";
3
+ const scope = globalThis;
4
+ const WorkletBase = scope.AudioWorkletProcessor ?? class {
5
+ port;
6
+ };
7
+ class AudioConvertProcessor extends WorkletBase {
8
+ #format;
9
+ #sampleRate;
10
+ #frames = 0;
11
+ #pending = [];
12
+ constructor(options) {
13
+ super(options);
14
+ this.#format = options?.processorOptions?.format ?? {
15
+ encoding: "pcm16",
16
+ sampleRate: 24e3,
17
+ channels: 1
18
+ };
19
+ const sampleRate = options?.processorOptions?.sampleRate;
20
+ this.#sampleRate = sampleRate !== void 0 && Number.isFinite(sampleRate) && sampleRate > 0 ? sampleRate : 48e3;
21
+ }
22
+ process(inputs) {
23
+ const input = inputs[0];
24
+ const channels = [];
25
+ if (input) {
26
+ for (let index = 0; index < input.length; index++) {
27
+ const channel = input[index];
28
+ if (channel) channels.push(channel);
29
+ }
30
+ }
31
+ const frames = channels[0]?.length ?? 0;
32
+ if (channels.length === 0 || frames === 0) return true;
33
+ const count = Math.min(channels.length, 2);
34
+ const data = new Uint8Array(frames * count * 4);
35
+ const view = new DataView(data.buffer);
36
+ for (let frame = 0; frame < frames; frame++) {
37
+ for (let channel = 0; channel < count; channel++) {
38
+ view.setFloat32((frame * count + channel) * 4, channels[channel]?.[frame] ?? 0, true);
39
+ }
40
+ }
41
+ const converted = convertAudio(
42
+ {
43
+ data,
44
+ format: {
45
+ encoding: "pcmf32",
46
+ sampleRate: this.#sampleRate,
47
+ channels: count === 1 ? 1 : 2
48
+ },
49
+ timestamp: this.#frames / this.#sampleRate * 1e3
50
+ },
51
+ this.#format
52
+ );
53
+ this.#frames += frames;
54
+ this.#pending.push(converted.data);
55
+ this.port?.postMessage({
56
+ data: converted.data,
57
+ format: converted.format,
58
+ timestamp: converted.timestamp
59
+ });
60
+ return true;
61
+ }
62
+ take() {
63
+ return this.#pending.splice(0);
64
+ }
65
+ }
66
+ function registerAudioConvertWorklet(target = scope) {
67
+ if (typeof target.registerProcessor !== "function") return;
68
+ target.registerProcessor(AUDIO_CONVERT_PROCESSOR, AudioConvertProcessor);
69
+ }
70
+ function audioConvertWorkletUrl() {
71
+ return new URL(import.meta.url).href;
72
+ }
73
+ registerAudioConvertWorklet();
74
+ export {
75
+ AUDIO_CONVERT_PROCESSOR,
76
+ AudioConvertProcessor,
77
+ audioConvertWorkletUrl,
78
+ registerAudioConvertWorklet
79
+ };
80
+ //# sourceMappingURL=convert.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/worklet/convert.ts"],
4
+ "sourcesContent": ["import type { AudioFormat } from '../audio.ts';\nimport { convertAudio } from '../convert.ts';\n\nexport const AUDIO_CONVERT_PROCESSOR = 'pi-audio-convert';\n\nexport interface AudioConvertProcessorOptions {\n processorOptions?: {\n format?: AudioFormat;\n sampleRate?: number;\n };\n}\n\ninterface WorkletScope {\n AudioWorkletProcessor?: new (\n options?: AudioConvertProcessorOptions\n ) => {\n port?: { postMessage(message: unknown): void };\n };\n registerProcessor?: (\n name: string,\n processor: new (options?: AudioConvertProcessorOptions) => object\n ) => void;\n}\n\nconst scope = globalThis as WorkletScope;\n\nconst WorkletBase =\n scope.AudioWorkletProcessor ??\n class {\n port?: { postMessage(message: unknown): void };\n };\n\nexport class AudioConvertProcessor extends WorkletBase {\n readonly #format: AudioFormat;\n readonly #sampleRate: number;\n #frames = 0;\n readonly #pending: Uint8Array[] = [];\n\n constructor(options?: AudioConvertProcessorOptions) {\n super(options);\n this.#format = options?.processorOptions?.format ?? {\n encoding: 'pcm16',\n sampleRate: 24000,\n channels: 1,\n };\n const sampleRate = options?.processorOptions?.sampleRate;\n this.#sampleRate =\n sampleRate !== undefined && Number.isFinite(sampleRate) && sampleRate > 0\n ? sampleRate\n : 48000;\n }\n\n process(inputs: Array<ArrayLike<Float32Array | undefined> | undefined>): boolean {\n const input = inputs[0];\n const channels: Float32Array[] = [];\n if (input) {\n for (let index = 0; index < input.length; index++) {\n const channel = input[index];\n if (channel) channels.push(channel);\n }\n }\n const frames = channels[0]?.length ?? 0;\n if (channels.length === 0 || frames === 0) return true;\n const count = Math.min(channels.length, 2);\n const data = new Uint8Array(frames * count * 4);\n const view = new DataView(data.buffer);\n for (let frame = 0; frame < frames; frame++) {\n for (let channel = 0; channel < count; channel++) {\n view.setFloat32((frame * count + channel) * 4, channels[channel]?.[frame] ?? 0, true);\n }\n }\n const converted = convertAudio(\n {\n data,\n format: {\n encoding: 'pcmf32',\n sampleRate: this.#sampleRate,\n channels: count === 1 ? 1 : 2,\n },\n timestamp: (this.#frames / this.#sampleRate) * 1000,\n },\n this.#format\n );\n this.#frames += frames;\n this.#pending.push(converted.data);\n this.port?.postMessage({\n data: converted.data,\n format: converted.format,\n timestamp: converted.timestamp,\n });\n return true;\n }\n\n take(): Uint8Array[] {\n return this.#pending.splice(0);\n }\n}\n\nexport function registerAudioConvertWorklet(target: WorkletScope = scope): void {\n if (typeof target.registerProcessor !== 'function') return;\n target.registerProcessor(AUDIO_CONVERT_PROCESSOR, AudioConvertProcessor);\n}\n\nexport function audioConvertWorkletUrl(): string {\n return new URL(import.meta.url).href;\n}\n\nregisterAudioConvertWorklet();\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAEtB,MAAM,0BAA0B;AAqBvC,MAAM,QAAQ;AAEd,MAAM,cACJ,MAAM,yBACN,MAAM;AAAA,EACJ;AACF;AAEK,MAAM,8BAA8B,YAAY;AAAA,EAC5C;AAAA,EACA;AAAA,EACT,UAAU;AAAA,EACD,WAAyB,CAAC;AAAA,EAEnC,YAAY,SAAwC;AAClD,UAAM,OAAO;AACb,SAAK,UAAU,SAAS,kBAAkB,UAAU;AAAA,MAClD,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,UAAU;AAAA,IACZ;AACA,UAAM,aAAa,SAAS,kBAAkB;AAC9C,SAAK,cACH,eAAe,UAAa,OAAO,SAAS,UAAU,KAAK,aAAa,IACpE,aACA;AAAA,EACR;AAAA,EAEA,QAAQ,QAAyE;AAC/E,UAAM,QAAQ,OAAO,CAAC;AACtB,UAAM,WAA2B,CAAC;AAClC,QAAI,OAAO;AACT,eAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;AACjD,cAAM,UAAU,MAAM,KAAK;AAC3B,YAAI,QAAS,UAAS,KAAK,OAAO;AAAA,MACpC;AAAA,IACF;AACA,UAAM,SAAS,SAAS,CAAC,GAAG,UAAU;AACtC,QAAI,SAAS,WAAW,KAAK,WAAW,EAAG,QAAO;AAClD,UAAM,QAAQ,KAAK,IAAI,SAAS,QAAQ,CAAC;AACzC,UAAM,OAAO,IAAI,WAAW,SAAS,QAAQ,CAAC;AAC9C,UAAM,OAAO,IAAI,SAAS,KAAK,MAAM;AACrC,aAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS;AAC3C,eAAS,UAAU,GAAG,UAAU,OAAO,WAAW;AAChD,aAAK,YAAY,QAAQ,QAAQ,WAAW,GAAG,SAAS,OAAO,IAAI,KAAK,KAAK,GAAG,IAAI;AAAA,MACtF;AAAA,IACF;AACA,UAAM,YAAY;AAAA,MAChB;AAAA,QACE;AAAA,QACA,QAAQ;AAAA,UACN,UAAU;AAAA,UACV,YAAY,KAAK;AAAA,UACjB,UAAU,UAAU,IAAI,IAAI;AAAA,QAC9B;AAAA,QACA,WAAY,KAAK,UAAU,KAAK,cAAe;AAAA,MACjD;AAAA,MACA,KAAK;AAAA,IACP;AACA,SAAK,WAAW;AAChB,SAAK,SAAS,KAAK,UAAU,IAAI;AACjC,SAAK,MAAM,YAAY;AAAA,MACrB,MAAM,UAAU;AAAA,MAChB,QAAQ,UAAU;AAAA,MAClB,WAAW,UAAU;AAAA,IACvB,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,OAAqB;AACnB,WAAO,KAAK,SAAS,OAAO,CAAC;AAAA,EAC/B;AACF;AAEO,SAAS,4BAA4B,SAAuB,OAAa;AAC9E,MAAI,OAAO,OAAO,sBAAsB,WAAY;AACpD,SAAO,kBAAkB,yBAAyB,qBAAqB;AACzE;AAEO,SAAS,yBAAiC;AAC/C,SAAO,IAAI,IAAI,YAAY,GAAG,EAAE;AAClC;AAEA,4BAA4B;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/pi-audio",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -29,6 +29,10 @@
29
29
  "./faux": {
30
30
  "types": "./dist/types/faux/index.d.ts",
31
31
  "default": "./dist/faux/index.js"
32
+ },
33
+ "./worklet": {
34
+ "types": "./dist/types/worklet/convert.d.ts",
35
+ "default": "./dist/worklet/convert.js"
32
36
  }
33
37
  },
34
38
  "files": [