@ai-ecoverse/pi-audio 1.0.0 → 1.1.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,7 @@ 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
12
 
13
13
  ## Development
14
14
 
@@ -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,137 @@
1
+ import { blankUsage } from "../blank-usage.js";
2
+ import {
3
+ registerVoiceModel
4
+ } from "../models.js";
5
+ import { registerRealtimeApiProvider } from "../registry.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
+ #usage = blankUsage();
14
+ constructor(model, calls) {
15
+ this.model = model;
16
+ this.#calls = calls;
17
+ }
18
+ get closed() {
19
+ return this.#closed;
20
+ }
21
+ push(event) {
22
+ if (this.#closed) return;
23
+ const waiter = this.#waiters.shift();
24
+ if (waiter) waiter({ value: event, done: false });
25
+ else this.#queue.push(event);
26
+ }
27
+ appendAudio(chunk) {
28
+ this.#calls.push({ method: "appendAudio", chunk });
29
+ }
30
+ appendVideoFrame(jpeg) {
31
+ this.#calls.push({ method: "appendVideoFrame", jpeg });
32
+ }
33
+ sendText(text) {
34
+ this.#calls.push({ method: "sendText", text });
35
+ }
36
+ inject(text, delivery) {
37
+ this.#calls.push({ method: "inject", text, delivery });
38
+ }
39
+ respond(callId, result, delivery) {
40
+ this.#calls.push({ method: "respond", callId, result, delivery });
41
+ }
42
+ interrupt(heard) {
43
+ this.#calls.push({ method: "interrupt", heard });
44
+ }
45
+ update(patch) {
46
+ this.#calls.push({ method: "update", patch });
47
+ }
48
+ async close() {
49
+ this.#calls.push({ method: "close" });
50
+ this.#closed = true;
51
+ while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });
52
+ return { usage: this.#usage };
53
+ }
54
+ async *[Symbol.asyncIterator]() {
55
+ while (true) {
56
+ const queued = this.#queue.shift();
57
+ if (queued) {
58
+ yield queued;
59
+ continue;
60
+ }
61
+ if (this.#closed) return;
62
+ const next = await new Promise((resolve) => this.#waiters.push(resolve));
63
+ if (next.done || next.value === void 0) return;
64
+ yield next.value;
65
+ }
66
+ }
67
+ }
68
+ const states = /* @__PURE__ */ new Map();
69
+ const apis = /* @__PURE__ */ new Map();
70
+ const nativeDelivery = {
71
+ interrupt: "native",
72
+ when_idle: "native",
73
+ silent: "native"
74
+ };
75
+ function registerFauxRealtime(options = {}) {
76
+ const model = {
77
+ id: options.id ?? "faux-realtime",
78
+ name: options.name ?? "Faux realtime",
79
+ api: options.api ?? "faux-realtime",
80
+ provider: options.provider ?? "faux",
81
+ baseUrl: "http://localhost:0",
82
+ type: "realtime",
83
+ transports: ["websocket"],
84
+ toolCalls: options.toolCalls ?? "blocking",
85
+ inputFormat: pcm(24e3),
86
+ outputFormat: pcm(24e3),
87
+ resumable: false,
88
+ ephemeralAuth: false,
89
+ voices: [{ id: "faux", name: "Faux" }],
90
+ provisioning: "none",
91
+ delivery: options.delivery ?? nativeDelivery,
92
+ truncation: options.truncation ?? "client",
93
+ liveToolUpdate: true,
94
+ cost: zeroCost
95
+ };
96
+ const state = { model, calls: [], events: options.events ?? [], pending: [] };
97
+ states.set(modelKey(model), state);
98
+ const unregisterModel = registerVoiceModel(model);
99
+ const release = shareApi(
100
+ apis,
101
+ model.api,
102
+ () => registerRealtimeApiProvider({ api: model.api, connect: connectFaux }),
103
+ () => [...states.values()].some((entry) => entry.model.api === model.api)
104
+ );
105
+ return {
106
+ model,
107
+ calls: state.calls,
108
+ emit(event) {
109
+ if (state.session) state.session.push(event);
110
+ if (!state.session || state.session.closed) state.pending.push(event);
111
+ },
112
+ setFlags(flags) {
113
+ if (flags.toolCalls) state.model.toolCalls = flags.toolCalls;
114
+ if (flags.delivery) state.model.delivery = flags.delivery;
115
+ if (flags.truncation) state.model.truncation = flags.truncation;
116
+ },
117
+ unregister() {
118
+ if (states.get(modelKey(model)) === state) states.delete(modelKey(model));
119
+ unregisterModel();
120
+ release();
121
+ }
122
+ };
123
+ }
124
+ async function connectFaux(model, _context, _options) {
125
+ const state = states.get(modelKey(model));
126
+ if (!state) throw new Error(`Unknown faux realtime model ${model.provider}/${model.id}`);
127
+ const session = new FauxSession(state.model, state.calls);
128
+ state.session = session;
129
+ for (const event of state.pending) session.push(event);
130
+ state.pending = [];
131
+ for (const event of state.events) session.push(event);
132
+ return session;
133
+ }
134
+ export {
135
+ registerFauxRealtime
136
+ };
137
+ //# 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 { 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": "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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,108 @@
1
+ import { blankUsage } from "../blank-usage.js";
2
+ import { VoiceRequestError } from "../errors.js";
3
+ import { registerVoiceModel } from "../models.js";
4
+ import { registerSpeechApiProvider } from "../registry.js";
5
+ import { SpeechEventStream } from "../speech.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
+ };
23
+ const state = {
24
+ model,
25
+ alignment: options.alignment ?? false,
26
+ chunkChars: Math.max(1, options.chunkChars ?? 4),
27
+ calls: []
28
+ };
29
+ states.set(modelKey(model), state);
30
+ const unregisterModel = registerVoiceModel(model);
31
+ const release = shareApi(
32
+ apis,
33
+ model.api,
34
+ () => registerSpeechApiProvider({ api: model.api, speak: speakFaux }),
35
+ () => [...states.values()].some((entry) => entry.model.api === model.api)
36
+ );
37
+ return {
38
+ model,
39
+ calls: state.calls,
40
+ setAlignment(alignment) {
41
+ state.alignment = alignment;
42
+ },
43
+ unregister() {
44
+ if (states.get(modelKey(model)) === state) states.delete(modelKey(model));
45
+ unregisterModel();
46
+ release();
47
+ }
48
+ };
49
+ }
50
+ function speakFaux(model, input, options) {
51
+ const stream = new SpeechEventStream();
52
+ const state = states.get(modelKey(model));
53
+ if (!state) {
54
+ stream.push({
55
+ type: "error",
56
+ reason: "error",
57
+ error: `Unknown faux speech model ${model.provider}/${model.id}`
58
+ });
59
+ return stream;
60
+ }
61
+ state.calls.push(input);
62
+ void pumpSpeech(state, input, options, stream);
63
+ return stream;
64
+ }
65
+ async function readText(input, signal) {
66
+ if (typeof input.text === "string") return input.text;
67
+ let text = "";
68
+ for await (const part of input.text) {
69
+ if (signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
70
+ text += part;
71
+ }
72
+ return text;
73
+ }
74
+ function alignmentFor(start, length) {
75
+ const startMs = [];
76
+ const durationMs = [];
77
+ for (let offset = 0; offset < length; offset++) {
78
+ startMs.push((start + offset) * 20);
79
+ durationMs.push(20);
80
+ }
81
+ return { startMs, durationMs };
82
+ }
83
+ async function pumpSpeech(state, input, options, stream) {
84
+ try {
85
+ if (options?.signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
86
+ const text = await readText(input, options?.signal);
87
+ const format = input.format ?? state.model.outputFormats[0];
88
+ if (!format) throw new Error("No output format");
89
+ stream.push({ type: "start", format });
90
+ for (let start = 0; start < text.length; start += state.chunkChars) {
91
+ const slice = text.slice(start, start + state.chunkChars);
92
+ stream.push({
93
+ type: "audio",
94
+ chunk: { data: encodeChars(slice), format, timestamp: start * 20 },
95
+ span: { itemId: state.model.id, start, end: start + slice.length },
96
+ ...state.alignment ? { alignment: alignmentFor(start, slice.length) } : {}
97
+ });
98
+ }
99
+ stream.push({ type: "done", usage: blankUsage() });
100
+ } catch (error) {
101
+ const reason = error instanceof VoiceRequestError ? error.reason : "error";
102
+ stream.push({ type: "error", reason, error: errorMessage(error) });
103
+ }
104
+ }
105
+ export {
106
+ registerFauxSpeech
107
+ };
108
+ //# 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 { 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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,152 @@
1
+ import { blankUsage } from "../blank-usage.js";
2
+ import { VoiceRequestError } from "../errors.js";
3
+ import { registerVoiceModel } from "../models.js";
4
+ import { registerTranscriptionApiProvider } from "../registry.js";
5
+ import {
6
+ TranscriptionEventStream
7
+ } from "../transcription.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
+ };
31
+ const state = { model, cues: options.cues, calls: [] };
32
+ states.set(modelKey(model), state);
33
+ const unregisterModel = registerVoiceModel(model);
34
+ const release = shareApi(
35
+ apis,
36
+ model.api,
37
+ () => registerTranscriptionApiProvider({
38
+ api: model.api,
39
+ transcribe: transcribeFaux,
40
+ listen: listenFaux
41
+ }),
42
+ () => [...states.values()].some((entry) => entry.model.api === model.api)
43
+ );
44
+ return {
45
+ model,
46
+ calls: state.calls,
47
+ setCues(cues) {
48
+ state.cues = cues;
49
+ },
50
+ unregister() {
51
+ if (states.get(modelKey(model)) === state) states.delete(modelKey(model));
52
+ unregisterModel();
53
+ release();
54
+ }
55
+ };
56
+ }
57
+ async function transcribeFaux(model, input, options) {
58
+ if (options?.signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
59
+ const state = requireState(model);
60
+ state.calls.push({ kind: "transcribe", input, chunks: [] });
61
+ const text = "url" in input ? input.url : decodeBytes(input.data);
62
+ return { text, usage: blankUsage() };
63
+ }
64
+ function listenFaux(model, audio, options) {
65
+ const stream = new TranscriptionEventStream();
66
+ let state;
67
+ try {
68
+ state = requireState(model);
69
+ } catch (error) {
70
+ stream.push({ type: "error", reason: "error", error: errorMessage(error) });
71
+ return stream;
72
+ }
73
+ void pumpListen(state, audio, options, stream);
74
+ return stream;
75
+ }
76
+ function createPump(stream) {
77
+ const pump = {
78
+ stream,
79
+ finished: false,
80
+ push(event) {
81
+ if (pump.finished) return;
82
+ if (event.type === "turn_end") pump.turnText = event.text;
83
+ stream.push(event);
84
+ if (event.type === "done" || event.type === "error") pump.finished = true;
85
+ }
86
+ };
87
+ return pump;
88
+ }
89
+ function cuesAt(cues, at) {
90
+ return cues.filter((cue) => cue.at === at).flatMap((cue) => cue.events);
91
+ }
92
+ function endCues(cues, count) {
93
+ return cues.filter((cue) => cue.at === "end" || typeof cue.at === "number" && cue.at > count).flatMap((cue) => cue.events);
94
+ }
95
+ function finishScript(pump, cues, text, at, count) {
96
+ if (pump.finished) return;
97
+ if (cues) for (const event of endCues(cues, count)) pump.push(event);
98
+ else {
99
+ pump.push({ type: "final", segmentId: "0", text });
100
+ pump.push({ type: "turn_end", text, at });
101
+ }
102
+ if (!pump.finished) {
103
+ pump.push({
104
+ type: "done",
105
+ transcript: { text: pump.turnText ?? text, usage: blankUsage() }
106
+ });
107
+ }
108
+ }
109
+ async function pumpListen(state, audio, options, stream) {
110
+ const pump = createPump(stream);
111
+ const call = { kind: "listen", chunks: [] };
112
+ state.calls.push(call);
113
+ let text = "";
114
+ let at = 0;
115
+ let count = 0;
116
+ try {
117
+ if (options?.signal?.aborted) {
118
+ pump.push({ type: "error", reason: "aborted", error: "aborted" });
119
+ return;
120
+ }
121
+ for (const event of cuesAt(state.cues ?? [], 0)) pump.push(event);
122
+ if (!pump.finished) {
123
+ for await (const chunk of audio) {
124
+ if (pump.finished || options?.signal?.aborted) break;
125
+ call.chunks.push(chunk);
126
+ count += 1;
127
+ at = chunk.timestamp;
128
+ text += decodeBytes(chunk.data);
129
+ deliverChunk(pump, state.cues, chunk, count, text);
130
+ }
131
+ }
132
+ if (options?.signal?.aborted && !pump.finished) {
133
+ pump.push({ type: "error", reason: "aborted", error: "aborted" });
134
+ return;
135
+ }
136
+ finishScript(pump, state.cues, text, at, count);
137
+ } catch (error) {
138
+ pump.push({ type: "error", reason: "error", error: errorMessage(error) });
139
+ }
140
+ }
141
+ function deliverChunk(pump, cues, chunk, count, text) {
142
+ if (cues) {
143
+ for (const event of cuesAt(cues, count)) pump.push(event);
144
+ return;
145
+ }
146
+ if (count === 1) pump.push({ type: "speech_start", at: chunk.timestamp });
147
+ pump.push({ type: "partial", segmentId: "0", text });
148
+ }
149
+ export {
150
+ registerFauxTranscription
151
+ };
152
+ //# 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 { 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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",
6
+ "names": []
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,53 @@
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
+ export type FauxRealtimeCall = {
5
+ method: 'appendAudio';
6
+ chunk: AudioChunk;
7
+ } | {
8
+ method: 'appendVideoFrame';
9
+ jpeg: Uint8Array;
10
+ } | {
11
+ method: 'sendText';
12
+ text: string;
13
+ } | {
14
+ method: 'inject';
15
+ text: string;
16
+ delivery: Delivery;
17
+ } | {
18
+ method: 'respond';
19
+ callId: string;
20
+ result: Parameters<RealtimeSession['respond']>[1];
21
+ delivery?: Delivery;
22
+ } | {
23
+ method: 'interrupt';
24
+ heard?: {
25
+ itemId: string;
26
+ heardMs: number;
27
+ };
28
+ } | {
29
+ method: 'update';
30
+ patch: Parameters<RealtimeSession['update']>[0];
31
+ } | {
32
+ method: 'close';
33
+ };
34
+ export interface FauxRealtimeFlags {
35
+ toolCalls?: RealtimeModel<string>['toolCalls'];
36
+ delivery?: Record<Delivery, DeliverySupport>;
37
+ truncation?: RealtimeModel<string>['truncation'];
38
+ }
39
+ export interface FauxRealtimeOptions extends FauxRealtimeFlags {
40
+ api?: string;
41
+ provider?: string;
42
+ id?: string;
43
+ name?: string;
44
+ events?: RealtimeEvent[];
45
+ }
46
+ export interface FauxRealtime {
47
+ model: RealtimeModel<string>;
48
+ calls: FauxRealtimeCall[];
49
+ emit(event: RealtimeEvent): void;
50
+ setFlags(flags: FauxRealtimeFlags): void;
51
+ unregister(): void;
52
+ }
53
+ 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,18 @@
1
+ import { type SpeechModel } from '../models.ts';
2
+ import { type SpeechInput } from '../speech.ts';
3
+ export interface FauxSpeechOptions {
4
+ api?: string;
5
+ provider?: string;
6
+ id?: string;
7
+ name?: string;
8
+ alignment?: boolean;
9
+ chunkChars?: number;
10
+ textStreaming?: boolean;
11
+ }
12
+ export interface FauxSpeech {
13
+ model: SpeechModel<string>;
14
+ calls: SpeechInput[];
15
+ setAlignment(alignment: boolean): void;
16
+ unregister(): void;
17
+ }
18
+ export declare function registerFauxSpeech(options?: FauxSpeechOptions): FauxSpeech;
@@ -0,0 +1,27 @@
1
+ import type { AudioChunk } from '../audio.ts';
2
+ import { type TranscriptionModel } from '../models.ts';
3
+ import { type AudioInput, type TranscriptionEvent } from '../transcription.ts';
4
+ export interface FauxTranscriptionCue {
5
+ at: number | 'end';
6
+ events: TranscriptionEvent[];
7
+ }
8
+ export interface FauxTranscriptionOptions {
9
+ api?: string;
10
+ provider?: string;
11
+ id?: string;
12
+ name?: string;
13
+ turnDetection?: TranscriptionModel<string>['turnDetection'];
14
+ cues?: FauxTranscriptionCue[];
15
+ }
16
+ export interface FauxTranscriptionCall {
17
+ kind: 'transcribe' | 'listen';
18
+ input?: AudioInput;
19
+ chunks: AudioChunk[];
20
+ }
21
+ export interface FauxTranscription {
22
+ model: TranscriptionModel<string>;
23
+ calls: FauxTranscriptionCall[];
24
+ setCues(cues: FauxTranscriptionCue[] | undefined): void;
25
+ unregister(): void;
26
+ }
27
+ export declare function registerFauxTranscription(options?: FauxTranscriptionOptions): FauxTranscription;
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.1.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
  }