@ai-ecoverse/pi-audio 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/audio.js +25 -0
- package/dist/audio.js.map +3 -3
- package/dist/faux/realtime.js +57 -12
- package/dist/faux/realtime.js.map +2 -2
- package/dist/index.js +9 -0
- package/dist/index.js.map +2 -2
- package/dist/realtime.js +93 -0
- package/dist/realtime.js.map +3 -3
- package/dist/registry.js +9 -0
- package/dist/registry.js.map +2 -2
- package/dist/talker.js +192 -0
- package/dist/talker.js.map +7 -0
- package/dist/types/audio.d.ts +2 -0
- package/dist/types/faux/realtime.d.ts +6 -2
- package/dist/types/index.d.ts +4 -1
- package/dist/types/realtime.d.ts +38 -2
- package/dist/types/talker.d.ts +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,14 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
25
25
|
|
|
26
26
|
`convertAudio` and `convertAudioSource` convert between `AudioFormat`s: PCM16, float32, G.711 μ-law and A-law, mono and stereo, sample rate, and PCM16 WAV. Opus, MP3 and FLAC pass through only when the format does not change. `encodeAudioBase64` and `decodeAudioBase64` are the wire helpers for JSON protocols. `AudioChunk.data` stays binary. The same converter runs in an AudioWorklet from `@ai-ecoverse/pi-audio/worklet` (`AUDIO_CONVERT_PROCESSOR`, `registerAudioConvertWorklet`). Adapters declare `inputFormats` and `outputFormats` and do not resample on their own.
|
|
27
27
|
|
|
28
|
+
`connectRealtime` rejects a transport the model does not list, and a `resumeHandle` when `resumable` is false, with `VoiceRequestError`. `realtimeSocket(url, options, protocols)` keeps that url and copies `protocols`. When `options.gateway` is set it uses `gateway.url` and puts the subprotocol `cf-aig-authorization.<token>` first, which is how a browser sends a Cloudflare AI Gateway credential on the same realtime protocol.
|
|
29
|
+
|
|
30
|
+
`planDelivery` reads `RealtimeModel.delivery`. An `unsupported` mode throws `VoiceRequestError`. `when_idle` with `approximate` support waits while the model is producing output and runs on `response_done`, `interrupt`, or `close`. Native `when_idle` is sent immediately so the server can schedule it. `planTruncation` adds `{ itemId, audioEndMs }` only when `truncation` is `client` and `interrupt` was given what the listener heard.
|
|
31
|
+
|
|
32
|
+
`RealtimeLedger` meters one session. Input seconds come from `appendAudio`. Output seconds come from each `output_audio` event: PCM, mulaw, and alaw from the byte length, and a compressed encoding from character alignment. `sendText` and each delivered `inject` add one text message. Tool results do not. `close()` prices those meters with `priceAudio`. Sum that usage once. `response_done.usage` is the adapter's per-response figure, not a second copy of the session total.
|
|
33
|
+
|
|
34
|
+
`talkerTools` is `ask_agent` and `read`. pi-audio defines `askAgent` (a pi `Tool` whose parameters are `request` and optional `context`) and the `Thinker` interface. The host implements `Thinker` and supplies `read`. `bindThinker` runs direct tools with `directTimeoutMs` and truncates their text to `directMaxChars`. On a blocking `ask_agent` it answers with `ack` immediately and injects the result later. On an async call it `respond`s with the result and `options.result`. Progress is injected with `options.progress`. `tool_call_cancelled` aborts the thinker. The binding also yields `thinker_progress` / `thinker_result` captions and `thinking` / `tool` / `idle` phases.
|
|
35
|
+
|
|
28
36
|
## Development
|
|
29
37
|
|
|
30
38
|
```sh
|
package/dist/audio.js
CHANGED
|
@@ -1 +1,26 @@
|
|
|
1
|
+
const sampleWidth = {
|
|
2
|
+
pcm16: 2,
|
|
3
|
+
pcmf32: 4,
|
|
4
|
+
mulaw: 1,
|
|
5
|
+
alaw: 1,
|
|
6
|
+
opus: 0,
|
|
7
|
+
mp3: 0,
|
|
8
|
+
wav: 0,
|
|
9
|
+
flac: 0
|
|
10
|
+
};
|
|
11
|
+
function chunkSeconds(chunk) {
|
|
12
|
+
const width = sampleWidth[chunk.format.encoding];
|
|
13
|
+
if (width === 0 || chunk.format.sampleRate === 0) return 0;
|
|
14
|
+
return chunk.data.byteLength / (width * chunk.format.channels * chunk.format.sampleRate);
|
|
15
|
+
}
|
|
16
|
+
function alignmentSeconds(alignment) {
|
|
17
|
+
if (alignment === void 0) return 0;
|
|
18
|
+
let ms = 0;
|
|
19
|
+
for (const duration of alignment.durationMs) ms += duration;
|
|
20
|
+
return ms / 1e3;
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
alignmentSeconds,
|
|
24
|
+
chunkSeconds
|
|
25
|
+
};
|
|
1
26
|
//# sourceMappingURL=audio.js.map
|
package/dist/audio.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [],
|
|
4
|
-
"sourcesContent": [],
|
|
5
|
-
"mappings": "",
|
|
3
|
+
"sources": ["../src/audio.ts"],
|
|
4
|
+
"sourcesContent": ["export type AudioEncoding = 'pcm16' | 'pcmf32' | 'mulaw' | 'alaw' | 'opus' | 'mp3' | 'wav' | 'flac';\n\nexport interface AudioFormat {\n encoding: AudioEncoding;\n sampleRate: number;\n channels: 1 | 2;\n}\n\nexport interface AudioChunk {\n data: Uint8Array;\n format: AudioFormat;\n timestamp: number;\n}\n\nexport type AudioSource = AsyncIterable<AudioChunk>;\n\nexport type TextSpan =\n | { entryId: string; contentIndex: number; start: number; end: number }\n | { itemId: string; start: number; end: number };\n\nexport interface CharAlignment {\n startMs: number[];\n durationMs: number[];\n}\n\nexport interface AudioTag {\n span: TextSpan;\n alignment?: CharAlignment;\n}\n\nexport interface AudioSink {\n write(chunk: AudioChunk, tag?: AudioTag): void;\n flush(): void;\n readonly playedMs: number;\n onPlayback(listener: (tag: AudioTag, offsetMs: number) => void): () => void;\n}\n\nconst sampleWidth: Record<AudioEncoding, number> = {\n pcm16: 2,\n pcmf32: 4,\n mulaw: 1,\n alaw: 1,\n opus: 0,\n mp3: 0,\n wav: 0,\n flac: 0,\n};\n\nexport function chunkSeconds(chunk: AudioChunk): number {\n const width = sampleWidth[chunk.format.encoding];\n if (width === 0 || chunk.format.sampleRate === 0) return 0;\n return chunk.data.byteLength / (width * chunk.format.channels * chunk.format.sampleRate);\n}\n\nexport function alignmentSeconds(alignment: CharAlignment | undefined): number {\n if (alignment === undefined) return 0;\n let ms = 0;\n for (const duration of alignment.durationMs) ms += duration;\n return ms / 1000;\n}\n"],
|
|
5
|
+
"mappings": "AAqCA,MAAM,cAA6C;AAAA,EACjD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AACR;AAEO,SAAS,aAAa,OAA2B;AACtD,QAAM,QAAQ,YAAY,MAAM,OAAO,QAAQ;AAC/C,MAAI,UAAU,KAAK,MAAM,OAAO,eAAe,EAAG,QAAO;AACzD,SAAO,MAAM,KAAK,cAAc,QAAQ,MAAM,OAAO,WAAW,MAAM,OAAO;AAC/E;AAEO,SAAS,iBAAiB,WAA8C;AAC7E,MAAI,cAAc,OAAW,QAAO;AACpC,MAAI,KAAK;AACT,aAAW,YAAY,UAAU,WAAY,OAAM;AACnD,SAAO,KAAK;AACd;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/faux/realtime.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { VoiceRequestError } from "../errors.js";
|
|
1
2
|
import {
|
|
2
3
|
registerVoiceModel
|
|
3
4
|
} from "../models.js";
|
|
5
|
+
import {
|
|
6
|
+
planTruncation,
|
|
7
|
+
RealtimeLedger,
|
|
8
|
+
realtimeSocket
|
|
9
|
+
} from "../realtime.js";
|
|
4
10
|
import { registerRealtimeApiProvider } from "../registry.js";
|
|
5
|
-
import { priceAudio } from "../usage.js";
|
|
6
11
|
import { modelKey, pcm, shareApi, zeroCost } from "./shared.js";
|
|
7
12
|
class FauxSession {
|
|
8
13
|
model;
|
|
9
14
|
#calls;
|
|
15
|
+
#ledger = new RealtimeLedger();
|
|
10
16
|
#queue = [];
|
|
11
17
|
#waiters = [];
|
|
12
18
|
#closed = false;
|
|
@@ -17,44 +23,79 @@ class FauxSession {
|
|
|
17
23
|
get closed() {
|
|
18
24
|
return this.#closed;
|
|
19
25
|
}
|
|
26
|
+
#assertOpen() {
|
|
27
|
+
if (this.#closed) throw new VoiceRequestError("error", "session is closed");
|
|
28
|
+
}
|
|
20
29
|
push(event) {
|
|
21
30
|
if (this.#closed) return;
|
|
31
|
+
this.#ledger.observe(event);
|
|
22
32
|
const waiter = this.#waiters.shift();
|
|
23
33
|
if (waiter) waiter({ value: event, done: false });
|
|
24
34
|
else this.#queue.push(event);
|
|
25
35
|
}
|
|
26
36
|
appendAudio(chunk) {
|
|
37
|
+
this.#assertOpen();
|
|
38
|
+
this.#ledger.addInput(chunk);
|
|
27
39
|
this.#calls.push({ method: "appendAudio", chunk });
|
|
28
40
|
}
|
|
29
41
|
appendVideoFrame(jpeg) {
|
|
42
|
+
this.#assertOpen();
|
|
43
|
+
if (this.model.video === void 0)
|
|
44
|
+
throw new VoiceRequestError("error", "video is unsupported");
|
|
30
45
|
this.#calls.push({ method: "appendVideoFrame", jpeg });
|
|
31
46
|
}
|
|
32
47
|
sendText(text) {
|
|
48
|
+
this.#assertOpen();
|
|
49
|
+
this.#ledger.addText();
|
|
33
50
|
this.#calls.push({ method: "sendText", text });
|
|
34
51
|
}
|
|
35
52
|
inject(text, delivery) {
|
|
36
|
-
this.#
|
|
53
|
+
this.#assertOpen();
|
|
54
|
+
const table = this.model.delivery;
|
|
55
|
+
this.#ledger.submit(table, delivery, () => {
|
|
56
|
+
this.#assertOpen();
|
|
57
|
+
this.#ledger.addText();
|
|
58
|
+
this.#calls.push({ method: "inject", text, delivery, support: table[delivery] });
|
|
59
|
+
});
|
|
37
60
|
}
|
|
38
61
|
respond(callId, result, delivery) {
|
|
39
|
-
this.#
|
|
62
|
+
this.#assertOpen();
|
|
63
|
+
const send = (support) => {
|
|
64
|
+
this.#calls.push({ method: "respond", callId, result, delivery, support });
|
|
65
|
+
};
|
|
66
|
+
if (delivery === void 0) {
|
|
67
|
+
send();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const table = this.model.delivery;
|
|
71
|
+
this.#ledger.submit(table, delivery, () => {
|
|
72
|
+
this.#assertOpen();
|
|
73
|
+
send(table[delivery]);
|
|
74
|
+
});
|
|
40
75
|
}
|
|
41
76
|
interrupt(heard) {
|
|
42
|
-
this.#
|
|
77
|
+
this.#assertOpen();
|
|
78
|
+
this.#calls.push({
|
|
79
|
+
method: "interrupt",
|
|
80
|
+
heard,
|
|
81
|
+
plan: planTruncation(this.model.truncation, heard)
|
|
82
|
+
});
|
|
83
|
+
this.#ledger.settle();
|
|
43
84
|
}
|
|
44
85
|
update(patch) {
|
|
86
|
+
this.#assertOpen();
|
|
87
|
+
if (patch.tools !== void 0 && !this.model.liveToolUpdate) {
|
|
88
|
+
throw new VoiceRequestError("error", "live tool update is unsupported");
|
|
89
|
+
}
|
|
45
90
|
this.#calls.push({ method: "update", patch });
|
|
46
91
|
}
|
|
47
92
|
async close() {
|
|
93
|
+
if (this.#closed) return { usage: this.#ledger.usage(this.model.audioCost) };
|
|
94
|
+
this.#ledger.settle();
|
|
48
95
|
this.#calls.push({ method: "close" });
|
|
49
96
|
this.#closed = true;
|
|
50
97
|
while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });
|
|
51
|
-
return {
|
|
52
|
-
usage: priceAudio(this.model.audioCost, {
|
|
53
|
-
inputSeconds: 0,
|
|
54
|
-
outputSeconds: 0,
|
|
55
|
-
textMessages: 0
|
|
56
|
-
})
|
|
57
|
-
};
|
|
98
|
+
return { usage: this.#ledger.usage(this.model.audioCost) };
|
|
58
99
|
}
|
|
59
100
|
async *[Symbol.asyncIterator]() {
|
|
60
101
|
while (true) {
|
|
@@ -111,6 +152,9 @@ function registerFauxRealtime(options = {}) {
|
|
|
111
152
|
return {
|
|
112
153
|
model,
|
|
113
154
|
calls: state.calls,
|
|
155
|
+
get socket() {
|
|
156
|
+
return state.socket;
|
|
157
|
+
},
|
|
114
158
|
emit(event) {
|
|
115
159
|
if (state.session) state.session.push(event);
|
|
116
160
|
if (!state.session || state.session.closed) state.pending.push(event);
|
|
@@ -127,9 +171,10 @@ function registerFauxRealtime(options = {}) {
|
|
|
127
171
|
}
|
|
128
172
|
};
|
|
129
173
|
}
|
|
130
|
-
async function connectFaux(model, _context,
|
|
174
|
+
async function connectFaux(model, _context, options) {
|
|
131
175
|
const state = states.get(modelKey(model));
|
|
132
176
|
if (!state) throw new Error(`Unknown faux realtime model ${model.provider}/${model.id}`);
|
|
177
|
+
state.socket = realtimeSocket(model.baseUrl, options, ["realtime"]);
|
|
133
178
|
const session = new FauxSession(state.model, state.calls);
|
|
134
179
|
state.session = session;
|
|
135
180
|
for (const event of state.pending) session.push(event);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/faux/realtime.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport {\n type Delivery,\n type DeliverySupport,\n type RealtimeModel,\n registerVoiceModel,\n} from '../models.ts';\nimport
|
|
5
|
-
"mappings": "AAEA;AAAA,EAIE;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["import type { Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport {\n type Delivery,\n type DeliverySupport,\n type RealtimeModel,\n registerVoiceModel,\n} from '../models.ts';\nimport {\n planTruncation,\n type RealtimeContext,\n type RealtimeEvent,\n RealtimeLedger,\n type RealtimeOptions,\n type RealtimeSession,\n type RealtimeSocket,\n realtimeSocket,\n type TruncationPlan,\n} from '../realtime.ts';\nimport { registerRealtimeApiProvider } from '../registry.ts';\nimport type { RealtimeAudioCost } from '../usage.ts';\nimport { modelKey, pcm, shareApi, zeroCost } from './shared.ts';\n\nexport type FauxRealtimeCall =\n | { method: 'appendAudio'; chunk: AudioChunk }\n | { method: 'appendVideoFrame'; jpeg: Uint8Array }\n | { method: 'sendText'; text: string }\n | { method: 'inject'; text: string; delivery: Delivery; support: DeliverySupport }\n | {\n method: 'respond';\n callId: string;\n result: Parameters<RealtimeSession['respond']>[1];\n delivery?: Delivery;\n support?: DeliverySupport;\n }\n | {\n method: 'interrupt';\n heard?: { itemId: string; heardMs: number };\n plan: TruncationPlan;\n }\n | { method: 'update'; patch: Parameters<RealtimeSession['update']>[0] }\n | { method: 'close' };\n\nexport interface FauxRealtimeFlags {\n toolCalls?: RealtimeModel<string>['toolCalls'];\n delivery?: Record<Delivery, DeliverySupport>;\n truncation?: RealtimeModel<string>['truncation'];\n}\n\nexport interface FauxRealtimeOptions extends FauxRealtimeFlags {\n api?: string;\n provider?: string;\n id?: string;\n name?: string;\n events?: RealtimeEvent[];\n audioCost?: RealtimeAudioCost;\n}\n\nexport interface FauxRealtime {\n model: RealtimeModel<string>;\n calls: FauxRealtimeCall[];\n readonly socket: RealtimeSocket | undefined;\n emit(event: RealtimeEvent): void;\n setFlags(flags: FauxRealtimeFlags): void;\n unregister(): void;\n}\n\ninterface IteratorResultEvent {\n value?: RealtimeEvent;\n done: boolean;\n}\n\nclass FauxSession implements RealtimeSession {\n readonly model: RealtimeModel<string>;\n readonly #calls: FauxRealtimeCall[];\n readonly #ledger = new RealtimeLedger();\n readonly #queue: RealtimeEvent[] = [];\n readonly #waiters: Array<(result: IteratorResultEvent) => void> = [];\n #closed = false;\n\n constructor(model: RealtimeModel<string>, calls: FauxRealtimeCall[]) {\n this.model = model;\n this.#calls = calls;\n }\n\n get closed(): boolean {\n return this.#closed;\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new VoiceRequestError('error', 'session is closed');\n }\n\n push(event: RealtimeEvent): void {\n if (this.#closed) return;\n this.#ledger.observe(event);\n const waiter = this.#waiters.shift();\n if (waiter) waiter({ value: event, done: false });\n else this.#queue.push(event);\n }\n\n appendAudio(chunk: AudioChunk): void {\n this.#assertOpen();\n this.#ledger.addInput(chunk);\n this.#calls.push({ method: 'appendAudio', chunk });\n }\n\n appendVideoFrame(jpeg: Uint8Array): void {\n this.#assertOpen();\n if (this.model.video === undefined)\n throw new VoiceRequestError('error', 'video is unsupported');\n this.#calls.push({ method: 'appendVideoFrame', jpeg });\n }\n\n sendText(text: string): void {\n this.#assertOpen();\n this.#ledger.addText();\n this.#calls.push({ method: 'sendText', text });\n }\n\n inject(text: string, delivery: Delivery): void {\n this.#assertOpen();\n const table = this.model.delivery;\n this.#ledger.submit(table, delivery, () => {\n this.#assertOpen();\n this.#ledger.addText();\n this.#calls.push({ method: 'inject', text, delivery, support: table[delivery] });\n });\n }\n\n respond(\n callId: string,\n result: Parameters<RealtimeSession['respond']>[1],\n delivery?: Delivery\n ): void {\n this.#assertOpen();\n const send = (support?: DeliverySupport) => {\n this.#calls.push({ method: 'respond', callId, result, delivery, support });\n };\n if (delivery === undefined) {\n send();\n return;\n }\n const table = this.model.delivery;\n this.#ledger.submit(table, delivery, () => {\n this.#assertOpen();\n send(table[delivery]);\n });\n }\n\n interrupt(heard?: { itemId: string; heardMs: number }): void {\n this.#assertOpen();\n this.#calls.push({\n method: 'interrupt',\n heard,\n plan: planTruncation(this.model.truncation, heard),\n });\n this.#ledger.settle();\n }\n\n update(patch: Parameters<RealtimeSession['update']>[0]): void {\n this.#assertOpen();\n if (patch.tools !== undefined && !this.model.liveToolUpdate) {\n throw new VoiceRequestError('error', 'live tool update is unsupported');\n }\n this.#calls.push({ method: 'update', patch });\n }\n\n async close(): Promise<{ usage: Usage }> {\n if (this.#closed) return { usage: this.#ledger.usage(this.model.audioCost) };\n this.#ledger.settle();\n this.#calls.push({ method: 'close' });\n this.#closed = true;\n while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });\n return { usage: this.#ledger.usage(this.model.audioCost) };\n }\n\n async *[Symbol.asyncIterator](): AsyncIterator<RealtimeEvent> {\n while (true) {\n const queued = this.#queue.shift();\n if (queued) {\n yield queued;\n continue;\n }\n if (this.#closed) return;\n const next = await new Promise<IteratorResultEvent>((resolve) => this.#waiters.push(resolve));\n if (next.done || next.value === undefined) return;\n yield next.value;\n }\n }\n}\n\ninterface RealtimeState {\n model: RealtimeModel<string>;\n calls: FauxRealtimeCall[];\n events: RealtimeEvent[];\n pending: RealtimeEvent[];\n socket?: RealtimeSocket;\n session?: FauxSession;\n}\n\nconst states = new Map<string, RealtimeState>();\nconst apis = new Map<string, () => void>();\n\nconst nativeDelivery: Record<Delivery, DeliverySupport> = {\n interrupt: 'native',\n when_idle: 'native',\n silent: 'native',\n};\n\nexport function registerFauxRealtime(options: FauxRealtimeOptions = {}): FauxRealtime {\n const model: RealtimeModel<string> = {\n id: options.id ?? 'faux-realtime',\n name: options.name ?? 'Faux realtime',\n api: options.api ?? 'faux-realtime',\n provider: options.provider ?? 'faux',\n baseUrl: 'http://localhost:0',\n type: 'realtime',\n transports: ['websocket'],\n toolCalls: options.toolCalls ?? 'blocking',\n inputFormat: pcm(24000),\n outputFormat: pcm(24000),\n resumable: false,\n ephemeralAuth: false,\n voices: [{ id: 'faux', name: 'Faux' }],\n provisioning: 'none',\n delivery: options.delivery ?? nativeDelivery,\n truncation: options.truncation ?? 'client',\n liveToolUpdate: true,\n cost: zeroCost,\n audioCost: options.audioCost ?? {},\n };\n const state: RealtimeState = { model, calls: [], events: options.events ?? [], pending: [] };\n states.set(modelKey(model), state);\n const unregisterModel = registerVoiceModel(model);\n const release = shareApi(\n apis,\n model.api,\n () => registerRealtimeApiProvider({ api: model.api, connect: connectFaux }),\n () => [...states.values()].some((entry) => entry.model.api === model.api)\n );\n return {\n model,\n calls: state.calls,\n get socket() {\n return state.socket;\n },\n emit(event) {\n if (state.session) state.session.push(event);\n if (!state.session || state.session.closed) state.pending.push(event);\n },\n setFlags(flags) {\n if (flags.toolCalls) state.model.toolCalls = flags.toolCalls;\n if (flags.delivery) state.model.delivery = flags.delivery;\n if (flags.truncation) state.model.truncation = flags.truncation;\n },\n unregister() {\n if (states.get(modelKey(model)) === state) states.delete(modelKey(model));\n unregisterModel();\n release();\n },\n };\n}\n\nasync function connectFaux(\n model: RealtimeModel<string>,\n _context: RealtimeContext,\n options?: RealtimeOptions\n): Promise<RealtimeSession> {\n const state = states.get(modelKey(model));\n if (!state) throw new Error(`Unknown faux realtime model ${model.provider}/${model.id}`);\n state.socket = realtimeSocket(model.baseUrl, options, ['realtime']);\n const session = new FauxSession(state.model, state.calls);\n state.session = session;\n for (const event of state.pending) session.push(event);\n state.pending = [];\n for (const event of state.events) session.push(event);\n return session;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
listVoiceModels,
|
|
13
13
|
registerVoiceModel
|
|
14
14
|
} from "./models.js";
|
|
15
|
+
import { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from "./realtime.js";
|
|
15
16
|
import {
|
|
16
17
|
connectRealtime,
|
|
17
18
|
listen,
|
|
@@ -22,14 +23,18 @@ import {
|
|
|
22
23
|
transcribe
|
|
23
24
|
} from "./registry.js";
|
|
24
25
|
import { SpeechEventStream } from "./speech.js";
|
|
26
|
+
import { askAgent, bindThinker, talkerTools } from "./talker.js";
|
|
25
27
|
import { TranscriptionEventStream } from "./transcription.js";
|
|
26
28
|
import { addAudioCounters, priceAudio } from "./usage.js";
|
|
27
29
|
import { energyVad } from "./vad.js";
|
|
28
30
|
export {
|
|
31
|
+
RealtimeLedger,
|
|
29
32
|
SpeechEventStream,
|
|
30
33
|
TranscriptionEventStream,
|
|
31
34
|
VoiceRequestError,
|
|
32
35
|
addAudioCounters,
|
|
36
|
+
askAgent,
|
|
37
|
+
bindThinker,
|
|
33
38
|
blankUsage,
|
|
34
39
|
connectRealtime,
|
|
35
40
|
convertAudio,
|
|
@@ -40,13 +45,17 @@ export {
|
|
|
40
45
|
getVoiceModel,
|
|
41
46
|
listVoiceModels,
|
|
42
47
|
listen,
|
|
48
|
+
planDelivery,
|
|
49
|
+
planTruncation,
|
|
43
50
|
priceAudio,
|
|
51
|
+
realtimeSocket,
|
|
44
52
|
registerRealtimeApiProvider,
|
|
45
53
|
registerSpeechApiProvider,
|
|
46
54
|
registerTranscriptionApiProvider,
|
|
47
55
|
registerVoiceModel,
|
|
48
56
|
resolveVoiceAuth,
|
|
49
57
|
speak,
|
|
58
|
+
talkerTools,
|
|
50
59
|
transcribe
|
|
51
60
|
};
|
|
52
61
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeToken,\n} from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
|
|
5
|
-
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type {\n BindThinkerOptions,\n BoundThinker,\n Thinker,\n ThinkerCaption,\n ThinkerPhase,\n ThinkerUpdate,\n} from './talker.ts';\nexport { askAgent, bindThinker, talkerTools } from './talker.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAiBlC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAYP,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,yBAAyB;AASlC,SAAS,UAAU,aAAa,mBAAmB;AASnD,SAAS,gCAAgC;AASzC,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/realtime.js
CHANGED
|
@@ -1 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alignmentSeconds,
|
|
3
|
+
chunkSeconds
|
|
4
|
+
} from "./audio.js";
|
|
5
|
+
import { VoiceRequestError } from "./errors.js";
|
|
6
|
+
import { priceAudio } from "./usage.js";
|
|
7
|
+
function planDelivery(delivery, requested) {
|
|
8
|
+
const support = delivery[requested];
|
|
9
|
+
if (support !== "native" && support !== "approximate") {
|
|
10
|
+
throw new VoiceRequestError("error", `unsupported delivery: ${requested}`);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
delivery: requested,
|
|
14
|
+
support,
|
|
15
|
+
waitForIdle: requested === "when_idle" && support === "approximate"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function planTruncation(truncation, heard) {
|
|
19
|
+
if (truncation === "client" && heard !== void 0) {
|
|
20
|
+
return { item: { itemId: heard.itemId, audioEndMs: heard.heardMs } };
|
|
21
|
+
}
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
function realtimeSocket(url, options, protocols = []) {
|
|
25
|
+
const gateway = options?.gateway;
|
|
26
|
+
if (gateway === void 0) return { url, protocols: [...protocols] };
|
|
27
|
+
return {
|
|
28
|
+
url: gateway.url,
|
|
29
|
+
protocols: [`cf-aig-authorization.${gateway.token}`, ...protocols]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
class RealtimeLedger {
|
|
33
|
+
#inputSeconds = 0;
|
|
34
|
+
#outputSeconds = 0;
|
|
35
|
+
#textMessages = 0;
|
|
36
|
+
#busy = false;
|
|
37
|
+
#held = [];
|
|
38
|
+
get inputSeconds() {
|
|
39
|
+
return this.#inputSeconds;
|
|
40
|
+
}
|
|
41
|
+
get outputSeconds() {
|
|
42
|
+
return this.#outputSeconds;
|
|
43
|
+
}
|
|
44
|
+
get textMessages() {
|
|
45
|
+
return this.#textMessages;
|
|
46
|
+
}
|
|
47
|
+
get busy() {
|
|
48
|
+
return this.#busy;
|
|
49
|
+
}
|
|
50
|
+
addInput(chunk) {
|
|
51
|
+
this.#inputSeconds += chunkSeconds(chunk);
|
|
52
|
+
}
|
|
53
|
+
addOutput(chunk, alignment) {
|
|
54
|
+
const seconds = chunkSeconds(chunk);
|
|
55
|
+
this.#outputSeconds += seconds > 0 ? seconds : alignmentSeconds(alignment);
|
|
56
|
+
}
|
|
57
|
+
addText() {
|
|
58
|
+
this.#textMessages += 1;
|
|
59
|
+
}
|
|
60
|
+
observe(event) {
|
|
61
|
+
if (event.type === "output_audio") this.addOutput(event.chunk, event.alignment);
|
|
62
|
+
if (event.type === "output_audio" || event.type === "output_transcript") this.#busy = true;
|
|
63
|
+
if (event.type === "response_done") this.#release();
|
|
64
|
+
}
|
|
65
|
+
submit(delivery, requested, run) {
|
|
66
|
+
const plan = planDelivery(delivery, requested);
|
|
67
|
+
if (plan.waitForIdle && this.#busy) this.#held.push(run);
|
|
68
|
+
else run();
|
|
69
|
+
return plan;
|
|
70
|
+
}
|
|
71
|
+
settle() {
|
|
72
|
+
this.#release();
|
|
73
|
+
}
|
|
74
|
+
usage(rates) {
|
|
75
|
+
return priceAudio(rates, {
|
|
76
|
+
inputSeconds: this.#inputSeconds,
|
|
77
|
+
outputSeconds: this.#outputSeconds,
|
|
78
|
+
textMessages: this.#textMessages
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
#release() {
|
|
82
|
+
this.#busy = false;
|
|
83
|
+
const held = this.#held;
|
|
84
|
+
this.#held = [];
|
|
85
|
+
for (const run of held) run();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
RealtimeLedger,
|
|
90
|
+
planDelivery,
|
|
91
|
+
planTruncation,
|
|
92
|
+
realtimeSocket
|
|
93
|
+
};
|
|
1
94
|
//# sourceMappingURL=realtime.js.map
|
package/dist/realtime.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": [],
|
|
4
|
-
"sourcesContent": [],
|
|
5
|
-
"mappings": "",
|
|
3
|
+
"sources": ["../src/realtime.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n Message,\n ProviderRequestOptions,\n Tool,\n ToolCall,\n ToolResultMessage,\n Usage,\n} from '@earendil-works/pi-ai';\nimport {\n type AudioChunk,\n alignmentSeconds,\n type CharAlignment,\n chunkSeconds,\n type TextSpan,\n} from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { Delivery, DeliverySupport, RealtimeModel } from './models.ts';\nimport { priceAudio, type RealtimeAudioCost } from './usage.ts';\n\nexport interface RealtimeContext {\n instructions: string;\n tools: Tool[];\n voice: string;\n history?: Message[];\n}\n\nexport interface RealtimeToken {\n value: string;\n expiresAt: number;\n model: string;\n}\n\nexport interface RealtimeOptions extends ProviderRequestOptions {\n token?: RealtimeToken;\n transport?: 'websocket' | 'webrtc';\n gateway?: { url: string; token: string };\n resumeHandle?: string;\n}\n\nexport type RealtimeEvent =\n | { type: 'session_start'; sessionId: string }\n | { type: 'input_speech_start' }\n | { type: 'input_transcript'; itemId: string; text: string; final: boolean }\n | {\n type: 'output_audio';\n itemId: string;\n chunk: AudioChunk;\n span?: TextSpan;\n alignment?: CharAlignment;\n }\n | { type: 'output_transcript'; itemId: string; text: string; final: boolean }\n | { type: 'tool_call'; call: ToolCall; blocking: boolean }\n | { type: 'tool_call_cancelled'; callId: string }\n | { type: 'response_done'; itemId: string; interrupted: boolean; usage: Usage }\n | { type: 'go_away'; timeLeftMs: number }\n | { type: 'resumption'; handle: string }\n | { type: 'error'; error: string; fatal: boolean };\n\nexport interface RealtimeSession extends AsyncIterable<RealtimeEvent> {\n readonly model: RealtimeModel<string>;\n appendAudio(chunk: AudioChunk): void;\n appendVideoFrame?(jpeg: Uint8Array): void;\n sendText(text: string): void;\n inject(text: string, delivery: Delivery): void;\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void;\n interrupt(heard?: { itemId: string; heardMs: number }): void;\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void;\n close(): Promise<{ usage: Usage }>;\n}\n\nexport interface RealtimeApiProvider<TApi extends string = string> {\n api: TApi;\n connect: (\n model: RealtimeModel<TApi>,\n context: RealtimeContext,\n options?: RealtimeOptions\n ) => Promise<RealtimeSession>;\n}\n\nexport interface DeliveryPlan {\n delivery: Delivery;\n support: DeliverySupport;\n waitForIdle: boolean;\n}\n\nexport interface TruncationPlan {\n item?: { itemId: string; audioEndMs: number };\n}\n\nexport interface RealtimeSocket {\n url: string;\n protocols: string[];\n}\n\nexport function planDelivery(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery\n): DeliveryPlan {\n const support = delivery[requested];\n if (support !== 'native' && support !== 'approximate') {\n throw new VoiceRequestError('error', `unsupported delivery: ${requested}`);\n }\n return {\n delivery: requested,\n support,\n waitForIdle: requested === 'when_idle' && support === 'approximate',\n };\n}\n\nexport function planTruncation(\n truncation: RealtimeModel<string>['truncation'],\n heard?: { itemId: string; heardMs: number }\n): TruncationPlan {\n if (truncation === 'client' && heard !== undefined) {\n return { item: { itemId: heard.itemId, audioEndMs: heard.heardMs } };\n }\n return {};\n}\n\nexport function realtimeSocket(\n url: string,\n options?: Pick<RealtimeOptions, 'gateway'>,\n protocols: readonly string[] = []\n): RealtimeSocket {\n const gateway = options?.gateway;\n if (gateway === undefined) return { url, protocols: [...protocols] };\n return {\n url: gateway.url,\n protocols: [`cf-aig-authorization.${gateway.token}`, ...protocols],\n };\n}\n\nexport class RealtimeLedger {\n #inputSeconds = 0;\n #outputSeconds = 0;\n #textMessages = 0;\n #busy = false;\n #held: Array<() => void> = [];\n\n get inputSeconds(): number {\n return this.#inputSeconds;\n }\n\n get outputSeconds(): number {\n return this.#outputSeconds;\n }\n\n get textMessages(): number {\n return this.#textMessages;\n }\n\n get busy(): boolean {\n return this.#busy;\n }\n\n addInput(chunk: AudioChunk): void {\n this.#inputSeconds += chunkSeconds(chunk);\n }\n\n addOutput(chunk: AudioChunk, alignment?: CharAlignment): void {\n const seconds = chunkSeconds(chunk);\n this.#outputSeconds += seconds > 0 ? seconds : alignmentSeconds(alignment);\n }\n\n addText(): void {\n this.#textMessages += 1;\n }\n\n observe(event: RealtimeEvent): void {\n if (event.type === 'output_audio') this.addOutput(event.chunk, event.alignment);\n if (event.type === 'output_audio' || event.type === 'output_transcript') this.#busy = true;\n if (event.type === 'response_done') this.#release();\n }\n\n submit(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery,\n run: () => void\n ): DeliveryPlan {\n const plan = planDelivery(delivery, requested);\n if (plan.waitForIdle && this.#busy) this.#held.push(run);\n else run();\n return plan;\n }\n\n settle(): void {\n this.#release();\n }\n\n usage(rates: RealtimeAudioCost): Usage {\n return priceAudio(rates, {\n inputSeconds: this.#inputSeconds,\n outputSeconds: this.#outputSeconds,\n textMessages: this.#textMessages,\n });\n }\n\n #release(): void {\n this.#busy = false;\n const held = this.#held;\n this.#held = [];\n for (const run of held) run();\n }\n}\n"],
|
|
5
|
+
"mappings": "AAQA;AAAA,EAEE;AAAA,EAEA;AAAA,OAEK;AACP,SAAS,yBAAyB;AAElC,SAAS,kBAA0C;AA6E5C,SAAS,aACd,UACA,WACc;AACd,QAAM,UAAU,SAAS,SAAS;AAClC,MAAI,YAAY,YAAY,YAAY,eAAe;AACrD,UAAM,IAAI,kBAAkB,SAAS,yBAAyB,SAAS,EAAE;AAAA,EAC3E;AACA,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA,aAAa,cAAc,eAAe,YAAY;AAAA,EACxD;AACF;AAEO,SAAS,eACd,YACA,OACgB;AAChB,MAAI,eAAe,YAAY,UAAU,QAAW;AAClD,WAAO,EAAE,MAAM,EAAE,QAAQ,MAAM,QAAQ,YAAY,MAAM,QAAQ,EAAE;AAAA,EACrE;AACA,SAAO,CAAC;AACV;AAEO,SAAS,eACd,KACA,SACA,YAA+B,CAAC,GAChB;AAChB,QAAM,UAAU,SAAS;AACzB,MAAI,YAAY,OAAW,QAAO,EAAE,KAAK,WAAW,CAAC,GAAG,SAAS,EAAE;AACnE,SAAO;AAAA,IACL,KAAK,QAAQ;AAAA,IACb,WAAW,CAAC,wBAAwB,QAAQ,KAAK,IAAI,GAAG,SAAS;AAAA,EACnE;AACF;AAEO,MAAM,eAAe;AAAA,EAC1B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,QAA2B,CAAC;AAAA,EAE5B,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,gBAAwB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,OAAgB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS,OAAyB;AAChC,SAAK,iBAAiB,aAAa,KAAK;AAAA,EAC1C;AAAA,EAEA,UAAU,OAAmB,WAAiC;AAC5D,UAAM,UAAU,aAAa,KAAK;AAClC,SAAK,kBAAkB,UAAU,IAAI,UAAU,iBAAiB,SAAS;AAAA,EAC3E;AAAA,EAEA,UAAgB;AACd,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,QAAQ,OAA4B;AAClC,QAAI,MAAM,SAAS,eAAgB,MAAK,UAAU,MAAM,OAAO,MAAM,SAAS;AAC9E,QAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,oBAAqB,MAAK,QAAQ;AACtF,QAAI,MAAM,SAAS,gBAAiB,MAAK,SAAS;AAAA,EACpD;AAAA,EAEA,OACE,UACA,WACA,KACc;AACd,UAAM,OAAO,aAAa,UAAU,SAAS;AAC7C,QAAI,KAAK,eAAe,KAAK,MAAO,MAAK,MAAM,KAAK,GAAG;AAAA,QAClD,KAAI;AACT,WAAO;AAAA,EACT;AAAA,EAEA,SAAe;AACb,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,OAAiC;AACrC,WAAO,WAAW,OAAO;AAAA,MACvB,cAAc,KAAK;AAAA,MACnB,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,IACrB,CAAC;AAAA,EACH;AAAA,EAEA,WAAiB;AACf,SAAK,QAAQ;AACb,UAAM,OAAO,KAAK;AAClB,SAAK,QAAQ,CAAC;AACd,eAAW,OAAO,KAAM,KAAI;AAAA,EAC9B;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { speakInSentences } from "./chunk-speech.js";
|
|
2
|
+
import { VoiceRequestError } from "./errors.js";
|
|
2
3
|
import { listenWithVad } from "./vad-listen.js";
|
|
3
4
|
const transcriptionApis = /* @__PURE__ */ new Map();
|
|
4
5
|
const speechApis = /* @__PURE__ */ new Map();
|
|
@@ -45,6 +46,14 @@ function speak(model, input, options) {
|
|
|
45
46
|
return speakModel(model, input, options);
|
|
46
47
|
}
|
|
47
48
|
function connectRealtime(model, context, options) {
|
|
49
|
+
if (options?.transport !== void 0 && !model.transports.includes(options.transport)) {
|
|
50
|
+
return Promise.reject(
|
|
51
|
+
new VoiceRequestError("error", `unsupported transport: ${options.transport}`)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (options?.resumeHandle !== void 0 && !model.resumable) {
|
|
55
|
+
return Promise.reject(new VoiceRequestError("error", "session is not resumable"));
|
|
56
|
+
}
|
|
48
57
|
const connect = realtimeApis.get(model.api)?.provider.connect;
|
|
49
58
|
if (!connect) return Promise.reject(missing(model.api));
|
|
50
59
|
return connect(model, context, options);
|
package/dist/registry.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/registry.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioSource } from './audio.ts';\nimport { speakInSentences } from './chunk-speech.ts';\nimport type { RealtimeModel, SpeechModel, TranscriptionModel } from './models.ts';\nimport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeOptions,\n RealtimeSession,\n} from './realtime.ts';\nimport type { SpeechApiProvider, SpeechEventStream, SpeechInput } from './speech.ts';\nimport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEventStream,\n} from './transcription.ts';\nimport { listenWithVad } from './vad-listen.ts';\n\ninterface TranscriptionEntry {\n provider: TranscriptionApiProvider;\n}\n\ninterface SpeechEntry {\n provider: SpeechApiProvider;\n}\n\ninterface RealtimeEntry {\n provider: RealtimeApiProvider;\n}\n\nconst transcriptionApis = new Map<string, TranscriptionEntry>();\nconst speechApis = new Map<string, SpeechEntry>();\nconst realtimeApis = new Map<string, RealtimeEntry>();\n\nfunction missing(api: string): Error {\n return new Error(`No API provider registered for api: ${api}`);\n}\n\nexport function registerTranscriptionApiProvider(provider: TranscriptionApiProvider): () => void {\n const entry = { provider };\n transcriptionApis.set(provider.api, entry);\n return () => {\n if (transcriptionApis.get(provider.api) === entry) transcriptionApis.delete(provider.api);\n };\n}\n\nexport function registerSpeechApiProvider(provider: SpeechApiProvider): () => void {\n const entry = { provider };\n speechApis.set(provider.api, entry);\n return () => {\n if (speechApis.get(provider.api) === entry) speechApis.delete(provider.api);\n };\n}\n\nexport function registerRealtimeApiProvider(provider: RealtimeApiProvider): () => void {\n const entry = { provider };\n realtimeApis.set(provider.api, entry);\n return () => {\n if (realtimeApis.get(provider.api) === entry) realtimeApis.delete(provider.api);\n };\n}\n\nexport function transcribe(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n): Promise<Transcript> {\n const transcribeModel = transcriptionApis.get(model.api)?.provider.transcribe;\n if (!transcribeModel) return Promise.reject(missing(model.api));\n return transcribeModel(model, input, options);\n}\n\nexport function listen(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n): TranscriptionEventStream {\n const listenModel = transcriptionApis.get(model.api)?.provider.listen;\n if (!listenModel) throw missing(model.api);\n if (model.turnDetection === 'none') return listenWithVad(listenModel, model, audio, options);\n return listenModel(model, audio, options);\n}\n\nexport function speak(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const speakModel = speechApis.get(model.api)?.provider.speak;\n if (!speakModel) throw missing(model.api);\n if (!model.textStreaming) return speakInSentences(speakModel, model, input, options);\n return speakModel(model, input, options);\n}\n\nexport function connectRealtime(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options?: RealtimeOptions\n): Promise<RealtimeSession> {\n const connect = realtimeApis.get(model.api)?.provider.connect;\n if (!connect) return Promise.reject(missing(model.api));\n return connect(model, context, options);\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioSource } from './audio.ts';\nimport { speakInSentences } from './chunk-speech.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { RealtimeModel, SpeechModel, TranscriptionModel } from './models.ts';\nimport type {\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeOptions,\n RealtimeSession,\n} from './realtime.ts';\nimport type { SpeechApiProvider, SpeechEventStream, SpeechInput } from './speech.ts';\nimport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEventStream,\n} from './transcription.ts';\nimport { listenWithVad } from './vad-listen.ts';\n\ninterface TranscriptionEntry {\n provider: TranscriptionApiProvider;\n}\n\ninterface SpeechEntry {\n provider: SpeechApiProvider;\n}\n\ninterface RealtimeEntry {\n provider: RealtimeApiProvider;\n}\n\nconst transcriptionApis = new Map<string, TranscriptionEntry>();\nconst speechApis = new Map<string, SpeechEntry>();\nconst realtimeApis = new Map<string, RealtimeEntry>();\n\nfunction missing(api: string): Error {\n return new Error(`No API provider registered for api: ${api}`);\n}\n\nexport function registerTranscriptionApiProvider(provider: TranscriptionApiProvider): () => void {\n const entry = { provider };\n transcriptionApis.set(provider.api, entry);\n return () => {\n if (transcriptionApis.get(provider.api) === entry) transcriptionApis.delete(provider.api);\n };\n}\n\nexport function registerSpeechApiProvider(provider: SpeechApiProvider): () => void {\n const entry = { provider };\n speechApis.set(provider.api, entry);\n return () => {\n if (speechApis.get(provider.api) === entry) speechApis.delete(provider.api);\n };\n}\n\nexport function registerRealtimeApiProvider(provider: RealtimeApiProvider): () => void {\n const entry = { provider };\n realtimeApis.set(provider.api, entry);\n return () => {\n if (realtimeApis.get(provider.api) === entry) realtimeApis.delete(provider.api);\n };\n}\n\nexport function transcribe(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n): Promise<Transcript> {\n const transcribeModel = transcriptionApis.get(model.api)?.provider.transcribe;\n if (!transcribeModel) return Promise.reject(missing(model.api));\n return transcribeModel(model, input, options);\n}\n\nexport function listen(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n): TranscriptionEventStream {\n const listenModel = transcriptionApis.get(model.api)?.provider.listen;\n if (!listenModel) throw missing(model.api);\n if (model.turnDetection === 'none') return listenWithVad(listenModel, model, audio, options);\n return listenModel(model, audio, options);\n}\n\nexport function speak(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const speakModel = speechApis.get(model.api)?.provider.speak;\n if (!speakModel) throw missing(model.api);\n if (!model.textStreaming) return speakInSentences(speakModel, model, input, options);\n return speakModel(model, input, options);\n}\n\nexport function connectRealtime(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options?: RealtimeOptions\n): Promise<RealtimeSession> {\n if (options?.transport !== undefined && !model.transports.includes(options.transport)) {\n return Promise.reject(\n new VoiceRequestError('error', `unsupported transport: ${options.transport}`)\n );\n }\n if (options?.resumeHandle !== undefined && !model.resumable) {\n return Promise.reject(new VoiceRequestError('error', 'session is not resumable'));\n }\n const connect = realtimeApis.get(model.api)?.provider.connect;\n if (!connect) return Promise.reject(missing(model.api));\n return connect(model, context, options);\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAgBlC,SAAS,qBAAqB;AAc9B,MAAM,oBAAoB,oBAAI,IAAgC;AAC9D,MAAM,aAAa,oBAAI,IAAyB;AAChD,MAAM,eAAe,oBAAI,IAA2B;AAEpD,SAAS,QAAQ,KAAoB;AACnC,SAAO,IAAI,MAAM,uCAAuC,GAAG,EAAE;AAC/D;AAEO,SAAS,iCAAiC,UAAgD;AAC/F,QAAM,QAAQ,EAAE,SAAS;AACzB,oBAAkB,IAAI,SAAS,KAAK,KAAK;AACzC,SAAO,MAAM;AACX,QAAI,kBAAkB,IAAI,SAAS,GAAG,MAAM,MAAO,mBAAkB,OAAO,SAAS,GAAG;AAAA,EAC1F;AACF;AAEO,SAAS,0BAA0B,UAAyC;AACjF,QAAM,QAAQ,EAAE,SAAS;AACzB,aAAW,IAAI,SAAS,KAAK,KAAK;AAClC,SAAO,MAAM;AACX,QAAI,WAAW,IAAI,SAAS,GAAG,MAAM,MAAO,YAAW,OAAO,SAAS,GAAG;AAAA,EAC5E;AACF;AAEO,SAAS,4BAA4B,UAA2C;AACrF,QAAM,QAAQ,EAAE,SAAS;AACzB,eAAa,IAAI,SAAS,KAAK,KAAK;AACpC,SAAO,MAAM;AACX,QAAI,aAAa,IAAI,SAAS,GAAG,MAAM,MAAO,cAAa,OAAO,SAAS,GAAG;AAAA,EAChF;AACF;AAEO,SAAS,WACd,OACA,OACA,SACqB;AACrB,QAAM,kBAAkB,kBAAkB,IAAI,MAAM,GAAG,GAAG,SAAS;AACnE,MAAI,CAAC,gBAAiB,QAAO,QAAQ,OAAO,QAAQ,MAAM,GAAG,CAAC;AAC9D,SAAO,gBAAgB,OAAO,OAAO,OAAO;AAC9C;AAEO,SAAS,OACd,OACA,OACA,SAC0B;AAC1B,QAAM,cAAc,kBAAkB,IAAI,MAAM,GAAG,GAAG,SAAS;AAC/D,MAAI,CAAC,YAAa,OAAM,QAAQ,MAAM,GAAG;AACzC,MAAI,MAAM,kBAAkB,OAAQ,QAAO,cAAc,aAAa,OAAO,OAAO,OAAO;AAC3F,SAAO,YAAY,OAAO,OAAO,OAAO;AAC1C;AAEO,SAAS,MACd,OACA,OACA,SACmB;AACnB,QAAM,aAAa,WAAW,IAAI,MAAM,GAAG,GAAG,SAAS;AACvD,MAAI,CAAC,WAAY,OAAM,QAAQ,MAAM,GAAG;AACxC,MAAI,CAAC,MAAM,cAAe,QAAO,iBAAiB,YAAY,OAAO,OAAO,OAAO;AACnF,SAAO,WAAW,OAAO,OAAO,OAAO;AACzC;AAEO,SAAS,gBACd,OACA,SACA,SAC0B;AAC1B,MAAI,SAAS,cAAc,UAAa,CAAC,MAAM,WAAW,SAAS,QAAQ,SAAS,GAAG;AACrF,WAAO,QAAQ;AAAA,MACb,IAAI,kBAAkB,SAAS,0BAA0B,QAAQ,SAAS,EAAE;AAAA,IAC9E;AAAA,EACF;AACA,MAAI,SAAS,iBAAiB,UAAa,CAAC,MAAM,WAAW;AAC3D,WAAO,QAAQ,OAAO,IAAI,kBAAkB,SAAS,0BAA0B,CAAC;AAAA,EAClF;AACA,QAAM,UAAU,aAAa,IAAI,MAAM,GAAG,GAAG,SAAS;AACtD,MAAI,CAAC,QAAS,QAAO,QAAQ,OAAO,QAAQ,MAAM,GAAG,CAAC;AACtD,SAAO,QAAQ,OAAO,SAAS,OAAO;AACxC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/talker.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Type
|
|
3
|
+
} from "@earendil-works/pi-ai";
|
|
4
|
+
import { VoiceRequestError } from "./errors.js";
|
|
5
|
+
const heardLimit = 20;
|
|
6
|
+
const talkerTools = ["ask_agent", "read"];
|
|
7
|
+
const askAgent = {
|
|
8
|
+
name: "ask_agent",
|
|
9
|
+
description: "Hand a request to the coding agent. Use for anything that needs files, the browser, tools or more than a few seconds of thought.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
request: Type.String(),
|
|
12
|
+
context: Type.Optional(Type.String())
|
|
13
|
+
})
|
|
14
|
+
};
|
|
15
|
+
class PushStream {
|
|
16
|
+
#queue = [];
|
|
17
|
+
#waiters = [];
|
|
18
|
+
#closed = false;
|
|
19
|
+
push(value) {
|
|
20
|
+
if (this.#closed) return;
|
|
21
|
+
const waiter = this.#waiters.shift();
|
|
22
|
+
if (waiter) waiter({ value, done: false });
|
|
23
|
+
else this.#queue.push(value);
|
|
24
|
+
}
|
|
25
|
+
close() {
|
|
26
|
+
if (this.#closed) return;
|
|
27
|
+
this.#closed = true;
|
|
28
|
+
while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });
|
|
29
|
+
}
|
|
30
|
+
async *[Symbol.asyncIterator]() {
|
|
31
|
+
while (true) {
|
|
32
|
+
const queued = this.#queue.shift();
|
|
33
|
+
if (queued !== void 0) {
|
|
34
|
+
yield queued;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (this.#closed) return;
|
|
38
|
+
const next = await new Promise(
|
|
39
|
+
(resolve) => this.#waiters.push(resolve)
|
|
40
|
+
);
|
|
41
|
+
if (next.done || next.value === void 0) return;
|
|
42
|
+
yield next.value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function send(action) {
|
|
47
|
+
try {
|
|
48
|
+
action();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!(error instanceof VoiceRequestError)) throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function textOf(message) {
|
|
54
|
+
return message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
55
|
+
}
|
|
56
|
+
function acknowledged(call, ack) {
|
|
57
|
+
return {
|
|
58
|
+
role: "toolResult",
|
|
59
|
+
toolCallId: call.id,
|
|
60
|
+
toolName: call.name,
|
|
61
|
+
content: [{ type: "text", text: ack }],
|
|
62
|
+
isError: false,
|
|
63
|
+
timestamp: Date.now()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function errorResult(call, error) {
|
|
67
|
+
return {
|
|
68
|
+
role: "toolResult",
|
|
69
|
+
toolCallId: call.id,
|
|
70
|
+
toolName: call.name,
|
|
71
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
72
|
+
isError: true,
|
|
73
|
+
timestamp: Date.now()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function capResult(result, maxChars) {
|
|
77
|
+
let left = maxChars;
|
|
78
|
+
const content = result.content.map((part) => {
|
|
79
|
+
if (part.type !== "text") return part;
|
|
80
|
+
if (left <= 0) return { ...part, text: "" };
|
|
81
|
+
if (part.text.length <= left) {
|
|
82
|
+
left -= part.text.length;
|
|
83
|
+
return part;
|
|
84
|
+
}
|
|
85
|
+
const text = part.text.slice(0, left);
|
|
86
|
+
left = 0;
|
|
87
|
+
return { ...part, text };
|
|
88
|
+
});
|
|
89
|
+
return { ...result, content };
|
|
90
|
+
}
|
|
91
|
+
function bindThinker(session, thinker, direct, options) {
|
|
92
|
+
const captions = new PushStream();
|
|
93
|
+
const phases = new PushStream();
|
|
94
|
+
const done = run(session, thinker, direct, options, captions, phases).finally(() => {
|
|
95
|
+
captions.close();
|
|
96
|
+
phases.close();
|
|
97
|
+
captions.close();
|
|
98
|
+
phases.close();
|
|
99
|
+
});
|
|
100
|
+
return { captions, phases, done };
|
|
101
|
+
}
|
|
102
|
+
async function run(session, thinker, direct, options, captions, phases) {
|
|
103
|
+
const heard = [];
|
|
104
|
+
const running = /* @__PURE__ */ new Map();
|
|
105
|
+
for await (const event of session) {
|
|
106
|
+
if (event.type === "input_transcript" && event.final) heard.push(`user: ${event.text}`);
|
|
107
|
+
if (event.type === "output_transcript" && event.final) heard.push(`talker: ${event.text}`);
|
|
108
|
+
if (event.type === "tool_call_cancelled") {
|
|
109
|
+
const job2 = running.get(event.callId);
|
|
110
|
+
if (!job2) continue;
|
|
111
|
+
job2.abort.abort();
|
|
112
|
+
if (!job2.answered) {
|
|
113
|
+
job2.answered = true;
|
|
114
|
+
send(() => session.respond(event.callId, errorResult(job2.call, new Error("cancelled"))));
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (event.type !== "tool_call") continue;
|
|
119
|
+
const call = event.call;
|
|
120
|
+
if (call.name !== askAgent.name) {
|
|
121
|
+
phases.push({
|
|
122
|
+
phase: "tool",
|
|
123
|
+
toolName: call.name,
|
|
124
|
+
toolCallId: call.id,
|
|
125
|
+
args: call.arguments
|
|
126
|
+
});
|
|
127
|
+
void direct(call, AbortSignal.timeout(options.directTimeoutMs)).then((result) => {
|
|
128
|
+
send(() => session.respond(call.id, capResult(result, options.directMaxChars)));
|
|
129
|
+
phases.push({ phase: "idle" });
|
|
130
|
+
}).catch((error) => {
|
|
131
|
+
send(() => session.respond(call.id, errorResult(call, error)));
|
|
132
|
+
phases.push({ phase: "idle" });
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const job = { abort: new AbortController(), answered: event.blocking, call };
|
|
137
|
+
running.set(call.id, job);
|
|
138
|
+
phases.push({ phase: "thinking" });
|
|
139
|
+
if (event.blocking) send(() => session.respond(call.id, acknowledged(call, options.ack)));
|
|
140
|
+
void followAsk(
|
|
141
|
+
session,
|
|
142
|
+
thinker,
|
|
143
|
+
options,
|
|
144
|
+
captions,
|
|
145
|
+
phases,
|
|
146
|
+
running,
|
|
147
|
+
heard,
|
|
148
|
+
call,
|
|
149
|
+
event.blocking,
|
|
150
|
+
job
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function followAsk(session, thinker, options, captions, phases, running, heard, call, blocking, job) {
|
|
155
|
+
try {
|
|
156
|
+
for await (const update of thinker.ask(
|
|
157
|
+
String(call.arguments.request),
|
|
158
|
+
heard.slice(-heardLimit),
|
|
159
|
+
job.abort.signal
|
|
160
|
+
)) {
|
|
161
|
+
if (job.abort.signal.aborted) break;
|
|
162
|
+
if (update.type === "progress") {
|
|
163
|
+
captions.push({ type: "thinker_progress", callId: call.id, text: update.text });
|
|
164
|
+
send(() => session.inject(update.text, options.progress));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
job.answered = true;
|
|
168
|
+
captions.push({
|
|
169
|
+
type: "thinker_result",
|
|
170
|
+
callId: call.id,
|
|
171
|
+
summary: textOf(update.message),
|
|
172
|
+
isError: update.message.isError
|
|
173
|
+
});
|
|
174
|
+
if (blocking) send(() => session.inject(textOf(update.message), options.result));
|
|
175
|
+
else send(() => session.respond(call.id, update.message, options.result));
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!job.abort.signal.aborted && !job.answered) {
|
|
179
|
+
job.answered = true;
|
|
180
|
+
send(() => session.respond(call.id, errorResult(call, error)));
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
running.delete(call.id);
|
|
184
|
+
phases.push({ phase: "idle" });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export {
|
|
188
|
+
askAgent,
|
|
189
|
+
bindThinker,
|
|
190
|
+
talkerTools
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=talker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/talker.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n type JsonObject,\n type Tool,\n type ToolCall,\n type ToolResultMessage,\n Type,\n} from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from './errors.ts';\nimport type { Delivery } from './models.ts';\nimport type { RealtimeSession } from './realtime.ts';\n\nconst heardLimit = 20;\n\nexport const talkerTools = ['ask_agent', 'read'] as const;\n\nexport const askAgent: Tool = {\n name: 'ask_agent',\n description:\n 'Hand a request to the coding agent. Use for anything that needs files, the browser, tools or more than a few seconds of thought.',\n parameters: Type.Object({\n request: Type.String(),\n context: Type.Optional(Type.String()),\n }),\n};\n\nexport type ThinkerUpdate =\n | { type: 'progress'; text: string }\n | { type: 'result'; message: ToolResultMessage };\n\nexport interface Thinker {\n ask(request: string, heard: string[], signal: AbortSignal): AsyncIterable<ThinkerUpdate>;\n}\n\nexport interface BindThinkerOptions {\n progress: Delivery;\n result: Delivery;\n ack: string;\n directTimeoutMs: number;\n directMaxChars: number;\n}\n\nexport type ThinkerCaption =\n | { type: 'thinker_progress'; callId: string; text: string }\n | { type: 'thinker_result'; callId: string; summary: string; isError: boolean };\n\nexport type ThinkerPhase =\n | { phase: 'thinking' }\n | { phase: 'tool'; toolName: string; toolCallId: string; args: JsonObject }\n | { phase: 'idle' };\n\nexport interface BoundThinker {\n captions: AsyncIterable<ThinkerCaption>;\n phases: AsyncIterable<ThinkerPhase>;\n done: Promise<void>;\n}\n\nclass PushStream<T> implements AsyncIterable<T> {\n readonly #queue: T[] = [];\n readonly #waiters: Array<(result: { value?: T; done: boolean }) => void> = [];\n #closed = false;\n\n push(value: T): void {\n if (this.#closed) return;\n const waiter = this.#waiters.shift();\n if (waiter) waiter({ value, done: false });\n else this.#queue.push(value);\n }\n\n close(): void {\n if (this.#closed) return;\n this.#closed = true;\n while (this.#waiters.length > 0) this.#waiters.shift()?.({ done: true });\n }\n\n async *[Symbol.asyncIterator](): AsyncIterator<T> {\n while (true) {\n const queued = this.#queue.shift();\n if (queued !== undefined) {\n yield queued;\n continue;\n }\n if (this.#closed) return;\n const next = await new Promise<{ value?: T; done: boolean }>((resolve) =>\n this.#waiters.push(resolve)\n );\n if (next.done || next.value === undefined) return;\n yield next.value;\n }\n }\n}\n\nfunction send(action: () => void): void {\n try {\n action();\n } catch (error) {\n if (!(error instanceof VoiceRequestError)) throw error;\n }\n}\n\nfunction textOf(message: ToolResultMessage): string {\n return message.content\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n');\n}\n\nfunction acknowledged(call: ToolCall, ack: string): ToolResultMessage {\n return {\n role: 'toolResult',\n toolCallId: call.id,\n toolName: call.name,\n content: [{ type: 'text', text: ack }],\n isError: false,\n timestamp: Date.now(),\n };\n}\n\nfunction errorResult(call: ToolCall, error: unknown): ToolResultMessage {\n return {\n role: 'toolResult',\n toolCallId: call.id,\n toolName: call.name,\n content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],\n isError: true,\n timestamp: Date.now(),\n };\n}\n\nfunction capResult(result: ToolResultMessage, maxChars: number): ToolResultMessage {\n let left = maxChars;\n const content = result.content.map((part) => {\n if (part.type !== 'text') return part;\n if (left <= 0) return { ...part, text: '' };\n if (part.text.length <= left) {\n left -= part.text.length;\n return part;\n }\n const text = part.text.slice(0, left);\n left = 0;\n return { ...part, text };\n });\n return { ...result, content };\n}\n\ninterface AskJob {\n abort: AbortController;\n answered: boolean;\n call: ToolCall;\n}\n\nexport function bindThinker(\n session: RealtimeSession,\n thinker: Thinker,\n direct: (call: ToolCall, signal: AbortSignal) => Promise<ToolResultMessage>,\n options: BindThinkerOptions\n): BoundThinker {\n const captions = new PushStream<ThinkerCaption>();\n const phases = new PushStream<ThinkerPhase>();\n const done = run(session, thinker, direct, options, captions, phases).finally(() => {\n captions.close();\n phases.close();\n captions.close();\n phases.close();\n });\n return { captions, phases, done };\n}\n\nasync function run(\n session: RealtimeSession,\n thinker: Thinker,\n direct: (call: ToolCall, signal: AbortSignal) => Promise<ToolResultMessage>,\n options: BindThinkerOptions,\n captions: PushStream<ThinkerCaption>,\n phases: PushStream<ThinkerPhase>\n): Promise<void> {\n const heard: string[] = [];\n const running = new Map<string, AskJob>();\n for await (const event of session) {\n if (event.type === 'input_transcript' && event.final) heard.push(`user: ${event.text}`);\n if (event.type === 'output_transcript' && event.final) heard.push(`talker: ${event.text}`);\n if (event.type === 'tool_call_cancelled') {\n const job = running.get(event.callId);\n if (!job) continue;\n job.abort.abort();\n if (!job.answered) {\n job.answered = true;\n send(() => session.respond(event.callId, errorResult(job.call, new Error('cancelled'))));\n }\n continue;\n }\n if (event.type !== 'tool_call') continue;\n const call = event.call;\n if (call.name !== askAgent.name) {\n phases.push({\n phase: 'tool',\n toolName: call.name,\n toolCallId: call.id,\n args: call.arguments,\n });\n void direct(call, AbortSignal.timeout(options.directTimeoutMs))\n .then((result) => {\n send(() => session.respond(call.id, capResult(result, options.directMaxChars)));\n phases.push({ phase: 'idle' });\n })\n .catch((error: unknown) => {\n send(() => session.respond(call.id, errorResult(call, error)));\n phases.push({ phase: 'idle' });\n });\n continue;\n }\n const job: AskJob = { abort: new AbortController(), answered: event.blocking, call };\n running.set(call.id, job);\n phases.push({ phase: 'thinking' });\n if (event.blocking) send(() => session.respond(call.id, acknowledged(call, options.ack)));\n void followAsk(\n session,\n thinker,\n options,\n captions,\n phases,\n running,\n heard,\n call,\n event.blocking,\n job\n );\n }\n}\n\nasync function followAsk(\n session: RealtimeSession,\n thinker: Thinker,\n options: BindThinkerOptions,\n captions: PushStream<ThinkerCaption>,\n phases: PushStream<ThinkerPhase>,\n running: Map<string, AskJob>,\n heard: string[],\n call: ToolCall,\n blocking: boolean,\n job: AskJob\n): Promise<void> {\n try {\n for await (const update of thinker.ask(\n String(call.arguments.request),\n heard.slice(-heardLimit),\n job.abort.signal\n )) {\n if (job.abort.signal.aborted) break;\n if (update.type === 'progress') {\n captions.push({ type: 'thinker_progress', callId: call.id, text: update.text });\n send(() => session.inject(update.text, options.progress));\n continue;\n }\n job.answered = true;\n captions.push({\n type: 'thinker_result',\n callId: call.id,\n summary: textOf(update.message),\n isError: update.message.isError,\n });\n if (blocking) send(() => session.inject(textOf(update.message), options.result));\n else send(() => session.respond(call.id, update.message, options.result));\n }\n } catch (error) {\n if (!job.abort.signal.aborted && !job.answered) {\n job.answered = true;\n send(() => session.respond(call.id, errorResult(call, error)));\n }\n } finally {\n running.delete(call.id);\n phases.push({ phase: 'idle' });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["job"]
|
|
7
|
+
}
|
package/dist/types/audio.d.ts
CHANGED
|
@@ -34,3 +34,5 @@ export interface AudioSink {
|
|
|
34
34
|
readonly playedMs: number;
|
|
35
35
|
onPlayback(listener: (tag: AudioTag, offsetMs: number) => void): () => void;
|
|
36
36
|
}
|
|
37
|
+
export declare function chunkSeconds(chunk: AudioChunk): number;
|
|
38
|
+
export declare function alignmentSeconds(alignment: CharAlignment | undefined): number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AudioChunk } from '../audio.ts';
|
|
2
2
|
import { type Delivery, type DeliverySupport, type RealtimeModel } from '../models.ts';
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
3
|
+
import { type RealtimeEvent, type RealtimeSession, type RealtimeSocket, type TruncationPlan } from '../realtime.ts';
|
|
4
|
+
import type { RealtimeAudioCost } from '../usage.ts';
|
|
5
5
|
export type FauxRealtimeCall = {
|
|
6
6
|
method: 'appendAudio';
|
|
7
7
|
chunk: AudioChunk;
|
|
@@ -15,17 +15,20 @@ export type FauxRealtimeCall = {
|
|
|
15
15
|
method: 'inject';
|
|
16
16
|
text: string;
|
|
17
17
|
delivery: Delivery;
|
|
18
|
+
support: DeliverySupport;
|
|
18
19
|
} | {
|
|
19
20
|
method: 'respond';
|
|
20
21
|
callId: string;
|
|
21
22
|
result: Parameters<RealtimeSession['respond']>[1];
|
|
22
23
|
delivery?: Delivery;
|
|
24
|
+
support?: DeliverySupport;
|
|
23
25
|
} | {
|
|
24
26
|
method: 'interrupt';
|
|
25
27
|
heard?: {
|
|
26
28
|
itemId: string;
|
|
27
29
|
heardMs: number;
|
|
28
30
|
};
|
|
31
|
+
plan: TruncationPlan;
|
|
29
32
|
} | {
|
|
30
33
|
method: 'update';
|
|
31
34
|
patch: Parameters<RealtimeSession['update']>[0];
|
|
@@ -48,6 +51,7 @@ export interface FauxRealtimeOptions extends FauxRealtimeFlags {
|
|
|
48
51
|
export interface FauxRealtime {
|
|
49
52
|
model: RealtimeModel<string>;
|
|
50
53
|
calls: FauxRealtimeCall[];
|
|
54
|
+
readonly socket: RealtimeSocket | undefined;
|
|
51
55
|
emit(event: RealtimeEvent): void;
|
|
52
56
|
setFlags(flags: FauxRealtimeFlags): void;
|
|
53
57
|
unregister(): void;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -5,10 +5,13 @@ export { convertAudio, convertAudioSource, decodeAudioBase64, encodeAudioBase64,
|
|
|
5
5
|
export { VoiceRequestError } from './errors.ts';
|
|
6
6
|
export type { Delivery, DeliverySupport, KnownRealtimeApi, KnownSpeechApi, KnownTranscriptionApi, RealtimeApi, RealtimeModel, SpeechApi, SpeechModel, TranscriptionApi, TranscriptionModel, VoiceInfo, VoiceModel, VoiceModelType, } from './models.ts';
|
|
7
7
|
export { getVoiceModel, listVoiceModels, registerVoiceModel, } from './models.ts';
|
|
8
|
-
export type { RealtimeApiProvider, RealtimeContext, RealtimeEvent, RealtimeOptions, RealtimeSession, RealtimeToken, } from './realtime.ts';
|
|
8
|
+
export type { DeliveryPlan, RealtimeApiProvider, RealtimeContext, RealtimeEvent, RealtimeOptions, RealtimeSession, RealtimeSocket, RealtimeToken, TruncationPlan, } from './realtime.ts';
|
|
9
|
+
export { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';
|
|
9
10
|
export { connectRealtime, listen, registerRealtimeApiProvider, registerSpeechApiProvider, registerTranscriptionApiProvider, speak, transcribe, } from './registry.ts';
|
|
10
11
|
export type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';
|
|
11
12
|
export { SpeechEventStream } from './speech.ts';
|
|
13
|
+
export type { BindThinkerOptions, BoundThinker, Thinker, ThinkerCaption, ThinkerPhase, ThinkerUpdate, } from './talker.ts';
|
|
14
|
+
export { askAgent, bindThinker, talkerTools } from './talker.ts';
|
|
12
15
|
export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, TranscriptionEvent, TranscriptSegment, } from './transcription.ts';
|
|
13
16
|
export { TranscriptionEventStream } from './transcription.ts';
|
|
14
17
|
export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
|
package/dist/types/realtime.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Message, ProviderRequestOptions, Tool, ToolCall, ToolResultMessage, Usage } from '@earendil-works/pi-ai';
|
|
2
|
-
import type
|
|
3
|
-
import type { Delivery, RealtimeModel } from './models.ts';
|
|
2
|
+
import { type AudioChunk, type CharAlignment, type TextSpan } from './audio.ts';
|
|
3
|
+
import type { Delivery, DeliverySupport, RealtimeModel } from './models.ts';
|
|
4
|
+
import { type RealtimeAudioCost } from './usage.ts';
|
|
4
5
|
export interface RealtimeContext {
|
|
5
6
|
instructions: string;
|
|
6
7
|
tools: Tool[];
|
|
@@ -85,3 +86,38 @@ export interface RealtimeApiProvider<TApi extends string = string> {
|
|
|
85
86
|
api: TApi;
|
|
86
87
|
connect: (model: RealtimeModel<TApi>, context: RealtimeContext, options?: RealtimeOptions) => Promise<RealtimeSession>;
|
|
87
88
|
}
|
|
89
|
+
export interface DeliveryPlan {
|
|
90
|
+
delivery: Delivery;
|
|
91
|
+
support: DeliverySupport;
|
|
92
|
+
waitForIdle: boolean;
|
|
93
|
+
}
|
|
94
|
+
export interface TruncationPlan {
|
|
95
|
+
item?: {
|
|
96
|
+
itemId: string;
|
|
97
|
+
audioEndMs: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export interface RealtimeSocket {
|
|
101
|
+
url: string;
|
|
102
|
+
protocols: string[];
|
|
103
|
+
}
|
|
104
|
+
export declare function planDelivery(delivery: Record<Delivery, DeliverySupport>, requested: Delivery): DeliveryPlan;
|
|
105
|
+
export declare function planTruncation(truncation: RealtimeModel<string>['truncation'], heard?: {
|
|
106
|
+
itemId: string;
|
|
107
|
+
heardMs: number;
|
|
108
|
+
}): TruncationPlan;
|
|
109
|
+
export declare function realtimeSocket(url: string, options?: Pick<RealtimeOptions, 'gateway'>, protocols?: readonly string[]): RealtimeSocket;
|
|
110
|
+
export declare class RealtimeLedger {
|
|
111
|
+
#private;
|
|
112
|
+
get inputSeconds(): number;
|
|
113
|
+
get outputSeconds(): number;
|
|
114
|
+
get textMessages(): number;
|
|
115
|
+
get busy(): boolean;
|
|
116
|
+
addInput(chunk: AudioChunk): void;
|
|
117
|
+
addOutput(chunk: AudioChunk, alignment?: CharAlignment): void;
|
|
118
|
+
addText(): void;
|
|
119
|
+
observe(event: RealtimeEvent): void;
|
|
120
|
+
submit(delivery: Record<Delivery, DeliverySupport>, requested: Delivery, run: () => void): DeliveryPlan;
|
|
121
|
+
settle(): void;
|
|
122
|
+
usage(rates: RealtimeAudioCost): Usage;
|
|
123
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type JsonObject, type Tool, type ToolCall, type ToolResultMessage } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { Delivery } from './models.ts';
|
|
3
|
+
import type { RealtimeSession } from './realtime.ts';
|
|
4
|
+
export declare const talkerTools: readonly ['ask_agent', 'read'];
|
|
5
|
+
export declare const askAgent: Tool;
|
|
6
|
+
export type ThinkerUpdate = {
|
|
7
|
+
type: 'progress';
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'result';
|
|
11
|
+
message: ToolResultMessage;
|
|
12
|
+
};
|
|
13
|
+
export interface Thinker {
|
|
14
|
+
ask(request: string, heard: string[], signal: AbortSignal): AsyncIterable<ThinkerUpdate>;
|
|
15
|
+
}
|
|
16
|
+
export interface BindThinkerOptions {
|
|
17
|
+
progress: Delivery;
|
|
18
|
+
result: Delivery;
|
|
19
|
+
ack: string;
|
|
20
|
+
directTimeoutMs: number;
|
|
21
|
+
directMaxChars: number;
|
|
22
|
+
}
|
|
23
|
+
export type ThinkerCaption = {
|
|
24
|
+
type: 'thinker_progress';
|
|
25
|
+
callId: string;
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'thinker_result';
|
|
29
|
+
callId: string;
|
|
30
|
+
summary: string;
|
|
31
|
+
isError: boolean;
|
|
32
|
+
};
|
|
33
|
+
export type ThinkerPhase = {
|
|
34
|
+
phase: 'thinking';
|
|
35
|
+
} | {
|
|
36
|
+
phase: 'tool';
|
|
37
|
+
toolName: string;
|
|
38
|
+
toolCallId: string;
|
|
39
|
+
args: JsonObject;
|
|
40
|
+
} | {
|
|
41
|
+
phase: 'idle';
|
|
42
|
+
};
|
|
43
|
+
export interface BoundThinker {
|
|
44
|
+
captions: AsyncIterable<ThinkerCaption>;
|
|
45
|
+
phases: AsyncIterable<ThinkerPhase>;
|
|
46
|
+
done: Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare function bindThinker(session: RealtimeSession, thinker: Thinker, direct: (call: ToolCall, signal: AbortSignal) => Promise<ToolResultMessage>, options: BindThinkerOptions): BoundThinker;
|
package/package.json
CHANGED