@ai-ecoverse/pi-audio 1.0.0 → 1.2.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
@@ -8,7 +8,18 @@ npm install @ai-ecoverse/pi-audio
8
8
 
9
9
  The package is a TypeScript ES module library for Node 24 and newer. It peers `@earendil-works/pi-ai`.
10
10
 
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.
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
+
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`.
12
23
 
13
24
  ## Development
14
25
 
@@ -0,0 +1,15 @@
1
+ import {
2
+ registerFauxRealtime
3
+ } from "./realtime.js";
4
+ import {
5
+ registerFauxSpeech
6
+ } from "./speech.js";
7
+ import {
8
+ registerFauxTranscription
9
+ } from "./transcription.js";
10
+ export {
11
+ registerFauxRealtime,
12
+ registerFauxSpeech,
13
+ registerFauxTranscription
14
+ };
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/faux/index.ts"],
4
+ "sourcesContent": ["export {\n type FauxRealtime,\n type FauxRealtimeCall,\n type FauxRealtimeFlags,\n type FauxRealtimeOptions,\n registerFauxRealtime,\n} from './realtime.ts';\nexport {\n type FauxSpeech,\n type FauxSpeechOptions,\n registerFauxSpeech,\n} from './speech.ts';\nexport {\n type FauxTranscription,\n type FauxTranscriptionCall,\n type FauxTranscriptionCue,\n type FauxTranscriptionOptions,\n registerFauxTranscription,\n} from './transcription.ts';\n"],
5
+ "mappings": "AAAA;AAAA,EAKE;AAAA,OACK;AACP;AAAA,EAGE;AAAA,OACK;AACP;AAAA,EAKE;AAAA,OACK;",
6
+ "names": []
7
+ }
@@ -0,0 +1,143 @@
1
+ import {
2
+ registerVoiceModel
3
+ } from "../models.js";
4
+ import { registerRealtimeApiProvider } from "../registry.js";
5
+ import { priceAudio } from "../usage.js";
6
+ import { modelKey, pcm, shareApi, zeroCost } from "./shared.js";
7
+ class FauxSession {
8
+ model;
9
+ #calls;
10
+ #queue = [];
11
+ #waiters = [];
12
+ #closed = false;
13
+ constructor(model, calls) {
14
+ this.model = model;
15
+ this.#calls = calls;
16
+ }
17
+ get closed() {
18
+ return this.#closed;
19
+ }
20
+ push(event) {
21
+ if (this.#closed) return;
22
+ const waiter = this.#waiters.shift();
23
+ if (waiter) waiter({ value: event, done: false });
24
+ else this.#queue.push(event);
25
+ }
26
+ appendAudio(chunk) {
27
+ this.#calls.push({ method: "appendAudio", chunk });
28
+ }
29
+ appendVideoFrame(jpeg) {
30
+ this.#calls.push({ method: "appendVideoFrame", jpeg });
31
+ }
32
+ sendText(text) {
33
+ this.#calls.push({ method: "sendText", text });
34
+ }
35
+ inject(text, delivery) {
36
+ this.#calls.push({ method: "inject", text, delivery });
37
+ }
38
+ respond(callId, result, delivery) {
39
+ this.#calls.push({ method: "respond", callId, result, delivery });
40
+ }
41
+ interrupt(heard) {
42
+ this.#calls.push({ method: "interrupt", heard });
43
+ }
44
+ update(patch) {
45
+ this.#calls.push({ method: "update", patch });
46
+ }
47
+ async close() {
48
+ this.#calls.push({ method: "close" });
49
+ this.#closed = true;
50
+ while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });
51
+ return {
52
+ usage: priceAudio(this.model.audioCost, {
53
+ inputSeconds: 0,
54
+ outputSeconds: 0,
55
+ textMessages: 0
56
+ })
57
+ };
58
+ }
59
+ async *[Symbol.asyncIterator]() {
60
+ while (true) {
61
+ const queued = this.#queue.shift();
62
+ if (queued) {
63
+ yield queued;
64
+ continue;
65
+ }
66
+ if (this.#closed) return;
67
+ const next = await new Promise((resolve) => this.#waiters.push(resolve));
68
+ if (next.done || next.value === void 0) return;
69
+ yield next.value;
70
+ }
71
+ }
72
+ }
73
+ const states = /* @__PURE__ */ new Map();
74
+ const apis = /* @__PURE__ */ new Map();
75
+ const nativeDelivery = {
76
+ interrupt: "native",
77
+ when_idle: "native",
78
+ silent: "native"
79
+ };
80
+ function registerFauxRealtime(options = {}) {
81
+ const model = {
82
+ id: options.id ?? "faux-realtime",
83
+ name: options.name ?? "Faux realtime",
84
+ api: options.api ?? "faux-realtime",
85
+ provider: options.provider ?? "faux",
86
+ baseUrl: "http://localhost:0",
87
+ type: "realtime",
88
+ transports: ["websocket"],
89
+ toolCalls: options.toolCalls ?? "blocking",
90
+ inputFormat: pcm(24e3),
91
+ outputFormat: pcm(24e3),
92
+ resumable: false,
93
+ ephemeralAuth: false,
94
+ voices: [{ id: "faux", name: "Faux" }],
95
+ provisioning: "none",
96
+ delivery: options.delivery ?? nativeDelivery,
97
+ truncation: options.truncation ?? "client",
98
+ liveToolUpdate: true,
99
+ cost: zeroCost,
100
+ audioCost: options.audioCost ?? {}
101
+ };
102
+ const state = { model, calls: [], events: options.events ?? [], pending: [] };
103
+ states.set(modelKey(model), state);
104
+ const unregisterModel = registerVoiceModel(model);
105
+ const release = shareApi(
106
+ apis,
107
+ model.api,
108
+ () => registerRealtimeApiProvider({ api: model.api, connect: connectFaux }),
109
+ () => [...states.values()].some((entry) => entry.model.api === model.api)
110
+ );
111
+ return {
112
+ model,
113
+ calls: state.calls,
114
+ emit(event) {
115
+ if (state.session) state.session.push(event);
116
+ if (!state.session || state.session.closed) state.pending.push(event);
117
+ },
118
+ setFlags(flags) {
119
+ if (flags.toolCalls) state.model.toolCalls = flags.toolCalls;
120
+ if (flags.delivery) state.model.delivery = flags.delivery;
121
+ if (flags.truncation) state.model.truncation = flags.truncation;
122
+ },
123
+ unregister() {
124
+ if (states.get(modelKey(model)) === state) states.delete(modelKey(model));
125
+ unregisterModel();
126
+ release();
127
+ }
128
+ };
129
+ }
130
+ async function connectFaux(model, _context, _options) {
131
+ const state = states.get(modelKey(model));
132
+ if (!state) throw new Error(`Unknown faux realtime model ${model.provider}/${model.id}`);
133
+ const session = new FauxSession(state.model, state.calls);
134
+ state.session = session;
135
+ for (const event of state.pending) session.push(event);
136
+ state.pending = [];
137
+ for (const event of state.events) session.push(event);
138
+ return session;
139
+ }
140
+ export {
141
+ registerFauxRealtime
142
+ };
143
+ //# sourceMappingURL=realtime.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/faux/realtime.ts"],
4
+ "sourcesContent": ["import type { Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport {\n type Delivery,\n type DeliverySupport,\n type RealtimeModel,\n registerVoiceModel,\n} from '../models.ts';\nimport 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
+ "names": []
7
+ }
@@ -0,0 +1,37 @@
1
+ const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
2
+ function pcm(sampleRate = 24e3) {
3
+ return { encoding: "pcm16", sampleRate, channels: 1 };
4
+ }
5
+ function modelKey(model) {
6
+ return `${model.provider}\0${model.id}`;
7
+ }
8
+ function decodeBytes(data) {
9
+ return new TextDecoder().decode(data);
10
+ }
11
+ function encodeChars(text) {
12
+ const bytes = new Uint8Array(text.length);
13
+ for (let index = 0; index < text.length; index++) bytes[index] = text.charCodeAt(index) & 255;
14
+ return bytes;
15
+ }
16
+ function errorMessage(error) {
17
+ return error instanceof Error ? error.message : String(error);
18
+ }
19
+ function shareApi(registrations, api, register, stillUsed) {
20
+ if (!registrations.has(api)) registrations.set(api, register());
21
+ return () => {
22
+ if (stillUsed()) return;
23
+ const unregister = registrations.get(api);
24
+ registrations.delete(api);
25
+ if (unregister) unregister();
26
+ };
27
+ }
28
+ export {
29
+ decodeBytes,
30
+ encodeChars,
31
+ errorMessage,
32
+ modelKey,
33
+ pcm,
34
+ shareApi,
35
+ zeroCost
36
+ };
37
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/faux/shared.ts"],
4
+ "sourcesContent": ["import type { AudioFormat } from '../audio.ts';\n\nexport const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\n\nexport function pcm(sampleRate = 24000): AudioFormat {\n return { encoding: 'pcm16', sampleRate, channels: 1 };\n}\n\nexport function modelKey(model: { provider: string; id: string }): string {\n return `${model.provider}\\0${model.id}`;\n}\n\nexport function decodeBytes(data: Uint8Array): string {\n return new TextDecoder().decode(data);\n}\n\nexport function encodeChars(text: string): Uint8Array {\n const bytes = new Uint8Array(text.length);\n for (let index = 0; index < text.length; index++) bytes[index] = text.charCodeAt(index) & 0xff;\n return bytes;\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function shareApi(\n registrations: Map<string, () => void>,\n api: string,\n register: () => () => void,\n stillUsed: () => boolean\n): () => void {\n if (!registrations.has(api)) registrations.set(api, register());\n return () => {\n if (stillUsed()) return;\n const unregister = registrations.get(api);\n registrations.delete(api);\n if (unregister) unregister();\n };\n}\n"],
5
+ "mappings": "AAEO,MAAM,WAAW,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,EAAE;AAEpE,SAAS,IAAI,aAAa,MAAoB;AACnD,SAAO,EAAE,UAAU,SAAS,YAAY,UAAU,EAAE;AACtD;AAEO,SAAS,SAAS,OAAiD;AACxE,SAAO,GAAG,MAAM,QAAQ,KAAK,MAAM,EAAE;AACvC;AAEO,SAAS,YAAY,MAA0B;AACpD,SAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AACtC;AAEO,SAAS,YAAY,MAA0B;AACpD,QAAM,QAAQ,IAAI,WAAW,KAAK,MAAM;AACxC,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,QAAS,OAAM,KAAK,IAAI,KAAK,WAAW,KAAK,IAAI;AAC1F,SAAO;AACT;AAEO,SAAS,aAAa,OAAwB;AACnD,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEO,SAAS,SACd,eACA,KACA,UACA,WACY;AACZ,MAAI,CAAC,cAAc,IAAI,GAAG,EAAG,eAAc,IAAI,KAAK,SAAS,CAAC;AAC9D,SAAO,MAAM;AACX,QAAI,UAAU,EAAG;AACjB,UAAM,aAAa,cAAc,IAAI,GAAG;AACxC,kBAAc,OAAO,GAAG;AACxB,QAAI,WAAY,YAAW;AAAA,EAC7B;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,115 @@
1
+ import { VoiceRequestError } from "../errors.js";
2
+ import { registerVoiceModel } from "../models.js";
3
+ import { registerSpeechApiProvider } from "../registry.js";
4
+ import { SpeechEventStream } from "../speech.js";
5
+ import { priceAudio } from "../usage.js";
6
+ import { encodeChars, errorMessage, modelKey, pcm, shareApi, zeroCost } from "./shared.js";
7
+ const states = /* @__PURE__ */ new Map();
8
+ const apis = /* @__PURE__ */ new Map();
9
+ function registerFauxSpeech(options = {}) {
10
+ const model = {
11
+ id: options.id ?? "faux-speech",
12
+ name: options.name ?? "Faux speech",
13
+ api: options.api ?? "faux-speech",
14
+ provider: options.provider ?? "faux",
15
+ baseUrl: "http://localhost:0",
16
+ type: "speech",
17
+ textStreaming: options.textStreaming ?? true,
18
+ outputFormats: [pcm()],
19
+ voices: [{ id: "faux", name: "Faux", languages: ["en"] }],
20
+ cloning: false,
21
+ cost: zeroCost,
22
+ audioCost: options.audioCost ?? {}
23
+ };
24
+ const state = {
25
+ model,
26
+ alignment: options.alignment ?? false,
27
+ chunkChars: Math.max(1, options.chunkChars ?? 4),
28
+ calls: []
29
+ };
30
+ states.set(modelKey(model), state);
31
+ const unregisterModel = registerVoiceModel(model);
32
+ const release = shareApi(
33
+ apis,
34
+ model.api,
35
+ () => registerSpeechApiProvider({ api: model.api, speak: speakFaux }),
36
+ () => [...states.values()].some((entry) => entry.model.api === model.api)
37
+ );
38
+ return {
39
+ model,
40
+ calls: state.calls,
41
+ setAlignment(alignment) {
42
+ state.alignment = alignment;
43
+ },
44
+ unregister() {
45
+ if (states.get(modelKey(model)) === state) states.delete(modelKey(model));
46
+ unregisterModel();
47
+ release();
48
+ }
49
+ };
50
+ }
51
+ function speakFaux(model, input, options) {
52
+ const stream = new SpeechEventStream();
53
+ const state = states.get(modelKey(model));
54
+ if (!state) {
55
+ stream.push({
56
+ type: "error",
57
+ reason: "error",
58
+ error: `Unknown faux speech model ${model.provider}/${model.id}`
59
+ });
60
+ return stream;
61
+ }
62
+ state.calls.push(input);
63
+ void pumpSpeech(state, input, options, stream);
64
+ return stream;
65
+ }
66
+ async function readText(input, signal) {
67
+ if (typeof input.text === "string") return input.text;
68
+ let text = "";
69
+ for await (const part of input.text) {
70
+ if (signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
71
+ text += part;
72
+ }
73
+ return text;
74
+ }
75
+ function alignmentFor(start, length) {
76
+ const startMs = [];
77
+ const durationMs = [];
78
+ for (let offset = 0; offset < length; offset++) {
79
+ startMs.push((start + offset) * 20);
80
+ durationMs.push(20);
81
+ }
82
+ return { startMs, durationMs };
83
+ }
84
+ async function pumpSpeech(state, input, options, stream) {
85
+ try {
86
+ if (options?.signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
87
+ const text = await readText(input, options?.signal);
88
+ const format = input.format ?? state.model.outputFormats[0];
89
+ if (!format) throw new Error("No output format");
90
+ stream.push({ type: "start", format });
91
+ for (let start = 0; start < text.length; start += state.chunkChars) {
92
+ const slice = text.slice(start, start + state.chunkChars);
93
+ stream.push({
94
+ type: "audio",
95
+ chunk: { data: encodeChars(slice), format, timestamp: start * 20 },
96
+ span: { itemId: state.model.id, start, end: start + slice.length },
97
+ ...state.alignment ? { alignment: alignmentFor(start, slice.length) } : {}
98
+ });
99
+ }
100
+ stream.push({
101
+ type: "done",
102
+ usage: priceAudio(state.model.audioCost, {
103
+ characters: text.length,
104
+ outputSeconds: text.length * 20 / 1e3
105
+ })
106
+ });
107
+ } catch (error) {
108
+ const reason = error instanceof VoiceRequestError ? error.reason : "error";
109
+ stream.push({ type: "error", reason, error: errorMessage(error) });
110
+ }
111
+ }
112
+ export {
113
+ registerFauxSpeech
114
+ };
115
+ //# sourceMappingURL=speech.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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 { 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
+ "names": []
7
+ }
@@ -0,0 +1,156 @@
1
+ import { VoiceRequestError } from "../errors.js";
2
+ import { registerVoiceModel } from "../models.js";
3
+ import { registerTranscriptionApiProvider } from "../registry.js";
4
+ import {
5
+ TranscriptionEventStream
6
+ } from "../transcription.js";
7
+ import { priceAudio } from "../usage.js";
8
+ import { decodeBytes, errorMessage, modelKey, pcm, shareApi, zeroCost } from "./shared.js";
9
+ const states = /* @__PURE__ */ new Map();
10
+ const apis = /* @__PURE__ */ new Map();
11
+ function requireState(model) {
12
+ const state = states.get(modelKey(model));
13
+ if (!state) throw new Error(`Unknown faux transcription model ${model.provider}/${model.id}`);
14
+ return state;
15
+ }
16
+ function registerFauxTranscription(options = {}) {
17
+ const model = {
18
+ id: options.id ?? "faux-transcription",
19
+ name: options.name ?? "Faux transcription",
20
+ api: options.api ?? "faux-transcription",
21
+ provider: options.provider ?? "faux",
22
+ baseUrl: "http://localhost:0",
23
+ type: "transcription",
24
+ streaming: true,
25
+ turnDetection: options.turnDetection ?? "vad",
26
+ inputFormats: [pcm(16e3)],
27
+ languages: ["en"],
28
+ diarization: false,
29
+ cost: zeroCost,
30
+ audioCost: options.audioCost ?? {}
31
+ };
32
+ const state = { model, cues: options.cues, calls: [] };
33
+ states.set(modelKey(model), state);
34
+ const unregisterModel = registerVoiceModel(model);
35
+ const release = shareApi(
36
+ apis,
37
+ model.api,
38
+ () => registerTranscriptionApiProvider({
39
+ api: model.api,
40
+ transcribe: transcribeFaux,
41
+ listen: listenFaux
42
+ }),
43
+ () => [...states.values()].some((entry) => entry.model.api === model.api)
44
+ );
45
+ return {
46
+ model,
47
+ calls: state.calls,
48
+ setCues(cues) {
49
+ state.cues = cues;
50
+ },
51
+ unregister() {
52
+ if (states.get(modelKey(model)) === state) states.delete(modelKey(model));
53
+ unregisterModel();
54
+ release();
55
+ }
56
+ };
57
+ }
58
+ async function transcribeFaux(model, input, options) {
59
+ if (options?.signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
60
+ const state = requireState(model);
61
+ state.calls.push({ kind: "transcribe", input, chunks: [] });
62
+ const text = "url" in input ? input.url : decodeBytes(input.data);
63
+ return { text, usage: priceAudio(state.model.audioCost, { inputSeconds: 0 }) };
64
+ }
65
+ function listenFaux(model, audio, options) {
66
+ const stream = new TranscriptionEventStream();
67
+ let state;
68
+ try {
69
+ state = requireState(model);
70
+ } catch (error) {
71
+ stream.push({ type: "error", reason: "error", error: errorMessage(error) });
72
+ return stream;
73
+ }
74
+ void pumpListen(state, audio, options, stream);
75
+ return stream;
76
+ }
77
+ function createPump(stream) {
78
+ const pump = {
79
+ stream,
80
+ finished: false,
81
+ push(event) {
82
+ if (pump.finished) return;
83
+ if (event.type === "turn_end") pump.turnText = event.text;
84
+ stream.push(event);
85
+ if (event.type === "done" || event.type === "error") pump.finished = true;
86
+ }
87
+ };
88
+ return pump;
89
+ }
90
+ function cuesAt(cues, at) {
91
+ return cues.filter((cue) => cue.at === at).flatMap((cue) => cue.events);
92
+ }
93
+ function endCues(cues, count) {
94
+ return cues.filter((cue) => cue.at === "end" || typeof cue.at === "number" && cue.at > count).flatMap((cue) => cue.events);
95
+ }
96
+ function finishScript(pump, cues, text, at, count, rates) {
97
+ if (pump.finished) return;
98
+ if (cues) for (const event of endCues(cues, count)) pump.push(event);
99
+ else {
100
+ pump.push({ type: "final", segmentId: "0", text });
101
+ pump.push({ type: "turn_end", text, at });
102
+ }
103
+ if (!pump.finished) {
104
+ pump.push({
105
+ type: "done",
106
+ transcript: {
107
+ text: pump.turnText ?? text,
108
+ usage: priceAudio(rates, { inputSeconds: at / 1e3 })
109
+ }
110
+ });
111
+ }
112
+ }
113
+ async function pumpListen(state, audio, options, stream) {
114
+ const pump = createPump(stream);
115
+ const call = { kind: "listen", chunks: [] };
116
+ state.calls.push(call);
117
+ let text = "";
118
+ let at = 0;
119
+ let count = 0;
120
+ try {
121
+ if (options?.signal?.aborted) {
122
+ pump.push({ type: "error", reason: "aborted", error: "aborted" });
123
+ return;
124
+ }
125
+ for (const event of cuesAt(state.cues ?? [], 0)) pump.push(event);
126
+ if (!pump.finished) {
127
+ for await (const chunk of audio) {
128
+ if (pump.finished || options?.signal?.aborted) break;
129
+ call.chunks.push(chunk);
130
+ count += 1;
131
+ at = chunk.timestamp;
132
+ text += decodeBytes(chunk.data);
133
+ deliverChunk(pump, state.cues, chunk, count, text);
134
+ }
135
+ }
136
+ if (options?.signal?.aborted && !pump.finished) {
137
+ pump.push({ type: "error", reason: "aborted", error: "aborted" });
138
+ return;
139
+ }
140
+ finishScript(pump, state.cues, text, at, count, state.model.audioCost);
141
+ } catch (error) {
142
+ pump.push({ type: "error", reason: "error", error: errorMessage(error) });
143
+ }
144
+ }
145
+ function deliverChunk(pump, cues, chunk, count, text) {
146
+ if (cues) {
147
+ for (const event of cuesAt(cues, count)) pump.push(event);
148
+ return;
149
+ }
150
+ if (count === 1) pump.push({ type: "speech_start", at: chunk.timestamp });
151
+ pump.push({ type: "partial", segmentId: "0", text });
152
+ }
153
+ export {
154
+ registerFauxTranscription
155
+ };
156
+ //# sourceMappingURL=transcription.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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 { 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": "AAEA,SAAS,yBAAyB;AAClC,SAAS,0BAAmD;AAC5D,SAAS,wCAAwC;AACjD;AAAA,EAIE;AAAA,OACK;AACP,SAAS,kBAA+C;AACxD,SAAS,aAAa,cAAc,UAAU,KAAK,UAAU,gBAAgB;AAoC7E,MAAM,SAAS,oBAAI,IAAgC;AACnD,MAAM,OAAO,oBAAI,IAAwB;AAEzC,SAAS,aAAa,OAAuD;AAC3E,QAAM,QAAQ,OAAO,IAAI,SAAS,KAAK,CAAC;AACxC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,oCAAoC,MAAM,QAAQ,IAAI,MAAM,EAAE,EAAE;AAC5F,SAAO;AACT;AAEO,SAAS,0BACd,UAAoC,CAAC,GAClB;AACnB,QAAM,QAAoC;AAAA,IACxC,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,WAAW;AAAA,IACX,eAAe,QAAQ,iBAAiB;AAAA,IACxC,cAAc,CAAC,IAAI,IAAK,CAAC;AAAA,IACzB,WAAW,CAAC,IAAI;AAAA,IAChB,aAAa;AAAA,IACb,MAAM;AAAA,IACN,WAAW,QAAQ,aAAa,CAAC;AAAA,EACnC;AACA,QAAM,QAA4B,EAAE,OAAO,MAAM,QAAQ,MAAM,OAAO,CAAC,EAAE;AACzE,SAAO,IAAI,SAAS,KAAK,GAAG,KAAK;AACjC,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,QAAM,UAAU;AAAA,IACd;AAAA,IACA,MAAM;AAAA,IACN,MACE,iCAAiC;AAAA,MAC/B,KAAK,MAAM;AAAA,MACX,YAAY;AAAA,MACZ,QAAQ;AAAA,IACV,CAAC;AAAA,IACH,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,QAAQ,MAAM;AACZ,YAAM,OAAO;AAAA,IACf;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,eACb,OACA,OACA,SACA;AACA,MAAI,SAAS,QAAQ,QAAS,OAAM,IAAI,kBAAkB,WAAW,SAAS;AAC9E,QAAM,QAAQ,aAAa,KAAK;AAChC,QAAM,MAAM,KAAK,EAAE,MAAM,cAAc,OAAO,QAAQ,CAAC,EAAE,CAAC;AAC1D,QAAM,OAAO,SAAS,QAAQ,MAAM,MAAM,YAAY,MAAM,IAAI;AAChE,SAAO,EAAE,MAAM,OAAO,WAAW,MAAM,MAAM,WAAW,EAAE,cAAc,EAAE,CAAC,EAAE;AAC/E;AAEA,SAAS,WACP,OACA,OACA,SAC0B;AAC1B,QAAM,SAAS,IAAI,yBAAyB;AAC5C,MAAI;AACJ,MAAI;AACF,YAAQ,aAAa,KAAK;AAAA,EAC5B,SAAS,OAAO;AACd,WAAO,KAAK,EAAE,MAAM,SAAS,QAAQ,SAAS,OAAO,aAAa,KAAK,EAAE,CAAC;AAC1E,WAAO;AAAA,EACT;AACA,OAAK,WAAW,OAAO,OAAO,SAAS,MAAM;AAC7C,SAAO;AACT;AASA,SAAS,WAAW,QAAwC;AAC1D,QAAM,OAAa;AAAA,IACjB;AAAA,IACA,UAAU;AAAA,IACV,KAAK,OAAO;AACV,UAAI,KAAK,SAAU;AACnB,UAAI,MAAM,SAAS,WAAY,MAAK,WAAW,MAAM;AACrD,aAAO,KAAK,KAAK;AACjB,UAAI,MAAM,SAAS,UAAU,MAAM,SAAS,QAAS,MAAK,WAAW;AAAA,IACvE;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,OAAO,MAA8B,IAA0C;AACtF,SAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,OAAO,EAAE,EAAE,QAAQ,CAAC,QAAQ,IAAI,MAAM;AACxE;AAEA,SAAS,QAAQ,MAA8B,OAAqC;AAClF,SAAO,KACJ,OAAO,CAAC,QAAQ,IAAI,OAAO,SAAU,OAAO,IAAI,OAAO,YAAY,IAAI,KAAK,KAAM,EAClF,QAAQ,CAAC,QAAQ,IAAI,MAAM;AAChC;AAEA,SAAS,aACP,MACA,MACA,MACA,IACA,OACA,OACA;AACA,MAAI,KAAK,SAAU;AACnB,MAAI,KAAM,YAAW,SAAS,QAAQ,MAAM,KAAK,EAAG,MAAK,KAAK,KAAK;AAAA,OAC9D;AACH,SAAK,KAAK,EAAE,MAAM,SAAS,WAAW,KAAK,KAAK,CAAC;AACjD,SAAK,KAAK,EAAE,MAAM,YAAY,MAAM,GAAG,CAAC;AAAA,EAC1C;AACA,MAAI,CAAC,KAAK,UAAU;AAClB,SAAK,KAAK;AAAA,MACR,MAAM;AAAA,MACN,YAAY;AAAA,QACV,MAAM,KAAK,YAAY;AAAA,QACvB,OAAO,WAAW,OAAO,EAAE,cAAc,KAAK,IAAK,CAAC;AAAA,MACtD;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,eAAe,WACb,OACA,OACA,SACA,QACA;AACA,QAAM,OAAO,WAAW,MAAM;AAC9B,QAAM,OAA8B,EAAE,MAAM,UAAU,QAAQ,CAAC,EAAE;AACjE,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,OAAO;AACX,MAAI,KAAK;AACT,MAAI,QAAQ;AACZ,MAAI;AACF,QAAI,SAAS,QAAQ,SAAS;AAC5B,WAAK,KAAK,EAAE,MAAM,SAAS,QAAQ,WAAW,OAAO,UAAU,CAAC;AAChE;AAAA,IACF;AACA,eAAW,SAAS,OAAO,MAAM,QAAQ,CAAC,GAAG,CAAC,EAAG,MAAK,KAAK,KAAK;AAChE,QAAI,CAAC,KAAK,UAAU;AAClB,uBAAiB,SAAS,OAAO;AAC/B,YAAI,KAAK,YAAY,SAAS,QAAQ,QAAS;AAC/C,aAAK,OAAO,KAAK,KAAK;AACtB,iBAAS;AACT,aAAK,MAAM;AACX,gBAAQ,YAAY,MAAM,IAAI;AAC9B,qBAAa,MAAM,MAAM,MAAM,OAAO,OAAO,IAAI;AAAA,MACnD;AAAA,IACF;AACA,QAAI,SAAS,QAAQ,WAAW,CAAC,KAAK,UAAU;AAC9C,WAAK,KAAK,EAAE,MAAM,SAAS,QAAQ,WAAW,OAAO,UAAU,CAAC;AAChE;AAAA,IACF;AACA,iBAAa,MAAM,MAAM,MAAM,MAAM,IAAI,OAAO,MAAM,MAAM,SAAS;AAAA,EACvE,SAAS,OAAO;AACd,SAAK,KAAK,EAAE,MAAM,SAAS,QAAQ,SAAS,OAAO,aAAa,KAAK,EAAE,CAAC;AAAA,EAC1E;AACF;AAEA,SAAS,aACP,MACA,MACA,OACA,OACA,MACA;AACA,MAAI,MAAM;AACR,eAAW,SAAS,OAAO,MAAM,KAAK,EAAG,MAAK,KAAK,KAAK;AACxD;AAAA,EACF;AACA,MAAI,UAAU,EAAG,MAAK,KAAK,EAAE,MAAM,gBAAgB,IAAI,MAAM,UAAU,CAAC;AACxE,OAAK,KAAK,EAAE,MAAM,WAAW,WAAW,KAAK,KAAK,CAAC;AACrD;",
6
+ "names": []
7
+ }
package/dist/index.js CHANGED
@@ -17,15 +17,18 @@ import {
17
17
  } from "./registry.js";
18
18
  import { SpeechEventStream } from "./speech.js";
19
19
  import { TranscriptionEventStream } from "./transcription.js";
20
+ import { addAudioCounters, priceAudio } from "./usage.js";
20
21
  export {
21
22
  SpeechEventStream,
22
23
  TranscriptionEventStream,
23
24
  VoiceRequestError,
25
+ addAudioCounters,
24
26
  blankUsage,
25
27
  connectRealtime,
26
28
  getVoiceModel,
27
29
  listVoiceModels,
28
30
  listen,
31
+ priceAudio,
29
32
  registerRealtimeApiProvider,
30
33
  registerSpeechApiProvider,
31
34
  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 { 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,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,3 @@
1
+ export { type FauxRealtime, type FauxRealtimeCall, type FauxRealtimeFlags, type FauxRealtimeOptions, registerFauxRealtime, } from './realtime.ts';
2
+ export { type FauxSpeech, type FauxSpeechOptions, registerFauxSpeech, } from './speech.ts';
3
+ export { type FauxTranscription, type FauxTranscriptionCall, type FauxTranscriptionCue, type FauxTranscriptionOptions, registerFauxTranscription, } from './transcription.ts';
@@ -0,0 +1,55 @@
1
+ import type { AudioChunk } from '../audio.ts';
2
+ import { type Delivery, type DeliverySupport, type RealtimeModel } from '../models.ts';
3
+ import type { RealtimeEvent, RealtimeSession } from '../realtime.ts';
4
+ import { type RealtimeAudioCost } from '../usage.ts';
5
+ export type FauxRealtimeCall = {
6
+ method: 'appendAudio';
7
+ chunk: AudioChunk;
8
+ } | {
9
+ method: 'appendVideoFrame';
10
+ jpeg: Uint8Array;
11
+ } | {
12
+ method: 'sendText';
13
+ text: string;
14
+ } | {
15
+ method: 'inject';
16
+ text: string;
17
+ delivery: Delivery;
18
+ } | {
19
+ method: 'respond';
20
+ callId: string;
21
+ result: Parameters<RealtimeSession['respond']>[1];
22
+ delivery?: Delivery;
23
+ } | {
24
+ method: 'interrupt';
25
+ heard?: {
26
+ itemId: string;
27
+ heardMs: number;
28
+ };
29
+ } | {
30
+ method: 'update';
31
+ patch: Parameters<RealtimeSession['update']>[0];
32
+ } | {
33
+ method: 'close';
34
+ };
35
+ export interface FauxRealtimeFlags {
36
+ toolCalls?: RealtimeModel<string>['toolCalls'];
37
+ delivery?: Record<Delivery, DeliverySupport>;
38
+ truncation?: RealtimeModel<string>['truncation'];
39
+ }
40
+ export interface FauxRealtimeOptions extends FauxRealtimeFlags {
41
+ api?: string;
42
+ provider?: string;
43
+ id?: string;
44
+ name?: string;
45
+ events?: RealtimeEvent[];
46
+ audioCost?: RealtimeAudioCost;
47
+ }
48
+ export interface FauxRealtime {
49
+ model: RealtimeModel<string>;
50
+ calls: FauxRealtimeCall[];
51
+ emit(event: RealtimeEvent): void;
52
+ setFlags(flags: FauxRealtimeFlags): void;
53
+ unregister(): void;
54
+ }
55
+ export declare function registerFauxRealtime(options?: FauxRealtimeOptions): FauxRealtime;
@@ -0,0 +1,16 @@
1
+ import type { AudioFormat } from '../audio.ts';
2
+ export declare const zeroCost: {
3
+ input: number;
4
+ output: number;
5
+ cacheRead: number;
6
+ cacheWrite: number;
7
+ };
8
+ export declare function pcm(sampleRate?: number): AudioFormat;
9
+ export declare function modelKey(model: {
10
+ provider: string;
11
+ id: string;
12
+ }): string;
13
+ export declare function decodeBytes(data: Uint8Array): string;
14
+ export declare function encodeChars(text: string): Uint8Array;
15
+ export declare function errorMessage(error: unknown): string;
16
+ export declare function shareApi(registrations: Map<string, () => void>, api: string, register: () => () => void, stillUsed: () => boolean): () => void;
@@ -0,0 +1,20 @@
1
+ import { type SpeechModel } from '../models.ts';
2
+ import { type SpeechInput } from '../speech.ts';
3
+ import { type SpeechAudioCost } from '../usage.ts';
4
+ export interface FauxSpeechOptions {
5
+ api?: string;
6
+ provider?: string;
7
+ id?: string;
8
+ name?: string;
9
+ alignment?: boolean;
10
+ chunkChars?: number;
11
+ textStreaming?: boolean;
12
+ audioCost?: SpeechAudioCost;
13
+ }
14
+ export interface FauxSpeech {
15
+ model: SpeechModel<string>;
16
+ calls: SpeechInput[];
17
+ setAlignment(alignment: boolean): void;
18
+ unregister(): void;
19
+ }
20
+ export declare function registerFauxSpeech(options?: FauxSpeechOptions): FauxSpeech;
@@ -0,0 +1,29 @@
1
+ import type { AudioChunk } from '../audio.ts';
2
+ import { type TranscriptionModel } from '../models.ts';
3
+ import { type AudioInput, type TranscriptionEvent } from '../transcription.ts';
4
+ import { type TranscriptionAudioCost } from '../usage.ts';
5
+ export interface FauxTranscriptionCue {
6
+ at: number | 'end';
7
+ events: TranscriptionEvent[];
8
+ }
9
+ export interface FauxTranscriptionOptions {
10
+ api?: string;
11
+ provider?: string;
12
+ id?: string;
13
+ name?: string;
14
+ turnDetection?: TranscriptionModel<string>['turnDetection'];
15
+ cues?: FauxTranscriptionCue[];
16
+ audioCost?: TranscriptionAudioCost;
17
+ }
18
+ export interface FauxTranscriptionCall {
19
+ kind: 'transcribe' | 'listen';
20
+ input?: AudioInput;
21
+ chunks: AudioChunk[];
22
+ }
23
+ export interface FauxTranscription {
24
+ model: TranscriptionModel<string>;
25
+ calls: FauxTranscriptionCall[];
26
+ setCues(cues: FauxTranscriptionCue[] | undefined): void;
27
+ unregister(): void;
28
+ }
29
+ export declare function registerFauxTranscription(options?: FauxTranscriptionOptions): FauxTranscription;
@@ -10,3 +10,5 @@ export type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';
10
10
  export { SpeechEventStream } from './speech.ts';
11
11
  export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, TranscriptionEvent, TranscriptSegment, } from './transcription.ts';
12
12
  export { TranscriptionEventStream } from './transcription.ts';
13
+ export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
14
+ 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;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/pi-audio",
3
- "version": "1.0.0",
3
+ "version": "1.2.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",
@@ -25,6 +25,10 @@
25
25
  ".": {
26
26
  "types": "./dist/types/index.d.ts",
27
27
  "default": "./dist/index.js"
28
+ },
29
+ "./faux": {
30
+ "types": "./dist/types/faux/index.d.ts",
31
+ "default": "./dist/faux/index.js"
28
32
  }
29
33
  },
30
34
  "files": [
@@ -47,7 +51,7 @@
47
51
  "devDependencies": {
48
52
  "@ai-ecoverse/slicc-shared-web": "1.21.0",
49
53
  "@biomejs/biome": "2.5.15",
50
- "@earendil-works/pi-ai": "1.0.4",
54
+ "@earendil-works/pi-ai": "1.1.0",
51
55
  "@types/node": "24.19.1",
52
56
  "esbuild": "0.28.2",
53
57
  "lefthook": "2.2.1",
@@ -56,6 +60,6 @@
56
60
  "typescript": "7.0.2"
57
61
  },
58
62
  "peerDependencies": {
59
- "@earendil-works/pi-ai": "1.0.4"
63
+ "@earendil-works/pi-ai": "1.1.0"
60
64
  }
61
65
  }