@ai-ecoverse/pi-audio 1.7.0 → 1.9.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/dist/turns.js ADDED
@@ -0,0 +1,347 @@
1
+ import { VoiceRequestError } from "./errors.js";
2
+ import { pushQueue } from "./queue.js";
3
+ import {
4
+ createSpokenState,
5
+ spokenPieces
6
+ } from "./spoken.js";
7
+ function voiceTurns(models, conversation, mic, out, options, signal) {
8
+ const phases = pushQueue();
9
+ const captions = pushQueue();
10
+ const session = { models, conversation, mic, out, options, signal, phases, captions };
11
+ const done = runSession(session).finally(() => {
12
+ phases.end();
13
+ captions.end();
14
+ });
15
+ return { phases: phases.iterable, captions: captions.iterable, done };
16
+ }
17
+ async function runSession(session) {
18
+ const mode = bargeMode(session.options);
19
+ let active;
20
+ let confidence;
21
+ let reserved = 0;
22
+ let openId = "";
23
+ const openTurn = () => {
24
+ if (!openId) {
25
+ reserved += 1;
26
+ openId = String(reserved);
27
+ }
28
+ return openId;
29
+ };
30
+ const stopPlayback = session.out.onPlayback((tag) => {
31
+ if (!active || !("entryId" in tag.span)) return;
32
+ active.cut = {
33
+ entryId: tag.span.entryId,
34
+ contentIndex: tag.span.contentIndex,
35
+ heardUpTo: tag.span.end,
36
+ heardMs: session.out.playedMs
37
+ };
38
+ });
39
+ const setPhase = phaseSetter(session.phases);
40
+ try {
41
+ if (session.signal.aborted) return;
42
+ setPhase({ phase: "listening" });
43
+ for await (const event of session.models.listen(session.options.ears, session.mic, {
44
+ signal: session.signal
45
+ })) {
46
+ if (event.type === "error") {
47
+ if (event.reason === "aborted" || session.signal.aborted) return;
48
+ throw new VoiceRequestError(event.reason, event.error);
49
+ }
50
+ const captionId = event.type === "partial" || event.type === "final" || event.type === "turn_end" ? openTurn() : openId;
51
+ confidence = noteUser(session, event, captionId, confidence);
52
+ if (shouldBarge(mode, event)) active = await interrupt(session, active);
53
+ if (event.type !== "turn_end") continue;
54
+ const turnId = openId || openTurn();
55
+ openId = "";
56
+ if (mode === "ignore") active = await finishTurn(active);
57
+ else active = await interrupt(session, active);
58
+ setPhase({ phase: "heard", text: event.text });
59
+ const meta = turnMeta(session.options, confidence);
60
+ confidence = void 0;
61
+ active = startTurn(session, setPhase, turnId, event, meta);
62
+ }
63
+ } finally {
64
+ stopPlayback();
65
+ if (session.signal.aborted && active) await interrupt(session, active);
66
+ else if (active) await active.done;
67
+ setPhase({ phase: "idle" });
68
+ }
69
+ }
70
+ function turnMeta(options, confidence) {
71
+ return {
72
+ source: "voice",
73
+ transcriptionModel: options.ears.id,
74
+ ...confidence === void 0 ? {} : { confidence }
75
+ };
76
+ }
77
+ function noteUser(session, event, turnId, confidence) {
78
+ if (event.type === "partial") {
79
+ session.captions.push({
80
+ type: "user_partial",
81
+ turnId,
82
+ segmentId: event.segmentId,
83
+ text: event.text
84
+ });
85
+ }
86
+ if (event.type === "final") {
87
+ session.captions.push({
88
+ type: "user_final",
89
+ turnId,
90
+ segmentId: event.segmentId,
91
+ text: event.text,
92
+ ...event.confidence === void 0 ? {} : { confidence: event.confidence }
93
+ });
94
+ return event.confidence ?? confidence;
95
+ }
96
+ if (event.type === "turn_end") {
97
+ session.captions.push({ type: "user_final", turnId, segmentId: "turn", text: event.text });
98
+ }
99
+ return confidence;
100
+ }
101
+ async function finishTurn(active) {
102
+ if (!active) return void 0;
103
+ await active.done;
104
+ return void 0;
105
+ }
106
+ function startTurn(session, setPhase, turnId, event, meta) {
107
+ const linked = linkSignal(session.signal);
108
+ const active = {
109
+ controller: linked.controller,
110
+ unlink: linked.unlink,
111
+ turnId,
112
+ utteranceId: `${turnId}:agent`,
113
+ done: Promise.resolve()
114
+ };
115
+ active.done = performTurn(session, setPhase, active, event.text, event.at, meta).finally(() => {
116
+ linked.unlink();
117
+ });
118
+ return active;
119
+ }
120
+ async function performTurn(session, setPhase, active, text, at, meta) {
121
+ const signal = active.controller.signal;
122
+ try {
123
+ setPhase({ phase: "thinking" });
124
+ const message = { role: "user", content: text, timestamp: at };
125
+ const feed = textFeed(signal);
126
+ const maps = [];
127
+ const producing = readAgent(session, setPhase, active, message, meta, feed, maps);
128
+ const audio = session.models.speak(
129
+ session.options.mouth,
130
+ { text: feed.iterable, voice: session.options.voice },
131
+ { signal }
132
+ );
133
+ await Promise.all([producing, pipeSpeech(session, setPhase, active, audio, maps)]);
134
+ if (!signal.aborted) setPhase({ phase: "listening" });
135
+ } catch (error) {
136
+ if (signal.aborted || session.signal.aborted) return;
137
+ throw error;
138
+ }
139
+ }
140
+ async function readAgent(session, setPhase, active, message, meta, feed, maps) {
141
+ const state = createSpokenState();
142
+ let spokenChars = 0;
143
+ let generated = "";
144
+ try {
145
+ for await (const event of session.conversation.prompt(
146
+ message,
147
+ meta,
148
+ active.controller.signal
149
+ )) {
150
+ if (active.controller.signal.aborted) break;
151
+ notePhase(setPhase, event);
152
+ for (const piece of spokenPieces(event, state, session.options)) {
153
+ spokenChars += piece.text.length;
154
+ generated = rememberPiece(
155
+ session,
156
+ active,
157
+ piece,
158
+ maps,
159
+ spokenChars,
160
+ generated,
161
+ state.entryId
162
+ );
163
+ feed.push(piece.text);
164
+ }
165
+ if (state.entryId) bindEntry(maps, state.entryId);
166
+ }
167
+ } finally {
168
+ feed.end();
169
+ }
170
+ }
171
+ function rememberPiece(session, active, piece, maps, spokenEnd, generated, entryId) {
172
+ const bound = piece.entryId ?? entryId;
173
+ maps.push({
174
+ spokenStart: spokenEnd - piece.text.length,
175
+ spokenEnd,
176
+ contentIndex: piece.contentIndex,
177
+ start: piece.start,
178
+ end: piece.end,
179
+ kind: piece.kind,
180
+ ...bound ? { entryId: bound } : {}
181
+ });
182
+ if (piece.kind === "announce") {
183
+ session.captions.push({ type: "announce", turnId: active.turnId, text: piece.text });
184
+ return generated;
185
+ }
186
+ const text = generated + piece.text;
187
+ session.captions.push({
188
+ type: "agent_generated",
189
+ turnId: active.turnId,
190
+ utteranceId: active.utteranceId,
191
+ text
192
+ });
193
+ return text;
194
+ }
195
+ async function pipeSpeech(session, setPhase, active, audio, maps) {
196
+ for await (const event of audio) {
197
+ if (active.controller.signal.aborted) return;
198
+ if (event.type === "error") {
199
+ if (event.reason === "aborted") return;
200
+ throw new VoiceRequestError(event.reason, event.error);
201
+ }
202
+ if (event.type !== "audio") continue;
203
+ setPhase({ phase: "speaking" });
204
+ const span = translateSpan(event.span, maps);
205
+ const tag = {
206
+ span,
207
+ ...event.alignment ? { alignment: event.alignment } : {}
208
+ };
209
+ session.out.write(event.chunk, tag);
210
+ }
211
+ }
212
+ function translateSpan(span, maps) {
213
+ if ("entryId" in span) return span;
214
+ const map = answerMap(span, maps);
215
+ if (!map?.entryId) return span;
216
+ const spokenLen = map.spokenEnd - map.spokenStart;
217
+ const sourceLen = map.end - map.start;
218
+ const start = map.start + scale(Math.max(0, span.start - map.spokenStart), spokenLen, sourceLen);
219
+ const end = map.start + scale(Math.min(spokenLen, span.end - map.spokenStart), spokenLen, sourceLen);
220
+ return { entryId: map.entryId, contentIndex: map.contentIndex, start, end };
221
+ }
222
+ function answerMap(span, maps) {
223
+ for (let index = maps.length - 1; index >= 0; index -= 1) {
224
+ const item = maps[index];
225
+ if (item?.kind !== "answer" || !item.entryId) continue;
226
+ if (item.spokenStart < span.end && item.spokenEnd > span.start) return item;
227
+ }
228
+ return void 0;
229
+ }
230
+ function scale(offset, spokenLen, sourceLen) {
231
+ if (spokenLen <= 0) return 0;
232
+ return Math.round(offset * sourceLen / spokenLen);
233
+ }
234
+ function bindEntry(maps, entryId) {
235
+ for (const map of maps) {
236
+ if (map.kind === "answer" && map.entryId === void 0) map.entryId = entryId;
237
+ }
238
+ }
239
+ function notePhase(setPhase, event) {
240
+ if (event.type === "run_start" || event.type === "turn_start") setPhase({ phase: "thinking" });
241
+ if (event.type === "compaction_start") setPhase({ phase: "compacting" });
242
+ if (event.type === "auto_retry_start" && event.attempt !== void 0 && event.at !== void 0) {
243
+ setPhase({ phase: "retrying", attempt: event.attempt, at: event.at });
244
+ }
245
+ if (event.type === "tool_execution_start" && event.toolName && event.toolCallId) {
246
+ setPhase({
247
+ phase: "tool",
248
+ toolName: event.toolName,
249
+ toolCallId: event.toolCallId,
250
+ args: event.args ?? {}
251
+ });
252
+ }
253
+ }
254
+ async function interrupt(session, active) {
255
+ if (!active) return void 0;
256
+ active.controller.abort();
257
+ session.out.flush();
258
+ await active.done;
259
+ if (active.cut) {
260
+ session.captions.push({
261
+ type: "agent_cut",
262
+ turnId: active.turnId,
263
+ utteranceId: active.utteranceId,
264
+ heardUpTo: active.cut.heardUpTo
265
+ });
266
+ await session.conversation.truncate(active.cut);
267
+ }
268
+ return void 0;
269
+ }
270
+ function bargeMode(options) {
271
+ const mode = options.bargeIn ?? "interrupt";
272
+ if (mode !== "auto") return mode;
273
+ if (options.echoRecommendation === "interrupt") return "interrupt";
274
+ if (options.echoRecommendation === "headphones") return "ignore";
275
+ return "onWords";
276
+ }
277
+ function shouldBarge(mode, event) {
278
+ if (mode === "ignore") return false;
279
+ if (mode === "interrupt") return event.type === "speech_start";
280
+ return event.type === "partial" && event.text.trim().length > 0;
281
+ }
282
+ function phaseSetter(phases) {
283
+ let previous = "";
284
+ return (phase) => {
285
+ const key = JSON.stringify(phase);
286
+ if (key === previous) return;
287
+ previous = key;
288
+ phases.push(phase);
289
+ };
290
+ }
291
+ function linkSignal(parent) {
292
+ const controller = new AbortController();
293
+ const abort = () => controller.abort();
294
+ if (parent.aborted) controller.abort();
295
+ else parent.addEventListener("abort", abort);
296
+ return {
297
+ controller,
298
+ unlink: () => parent.removeEventListener("abort", abort)
299
+ };
300
+ }
301
+ function textFeed(signal) {
302
+ const queue = [];
303
+ let ended = false;
304
+ let wake;
305
+ const notify = () => {
306
+ const current = wake;
307
+ wake = void 0;
308
+ current?.();
309
+ };
310
+ const onAbort = () => notify();
311
+ signal.addEventListener("abort", onAbort);
312
+ return {
313
+ push(text) {
314
+ if (ended || signal.aborted || text.length === 0) return;
315
+ queue.push(text);
316
+ notify();
317
+ },
318
+ end() {
319
+ ended = true;
320
+ notify();
321
+ },
322
+ iterable: {
323
+ async *[Symbol.asyncIterator]() {
324
+ try {
325
+ while (!signal.aborted) {
326
+ const next = queue.shift();
327
+ if (next !== void 0) {
328
+ yield next;
329
+ continue;
330
+ }
331
+ if (ended) return;
332
+ await new Promise((resolve) => {
333
+ if (queue.length > 0 || ended || signal.aborted) resolve();
334
+ else wake = resolve;
335
+ });
336
+ }
337
+ } finally {
338
+ signal.removeEventListener("abort", onAbort);
339
+ }
340
+ }
341
+ }
342
+ };
343
+ }
344
+ export {
345
+ voiceTurns
346
+ };
347
+ //# sourceMappingURL=turns.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/turns.ts"],
4
+ "sourcesContent": ["import type {\n AssistantMessageEvent,\n JsonObject,\n ProviderRequestOptions,\n ToolCall,\n UserMessage,\n} from '@earendil-works/pi-ai';\nimport type { AudioSink, AudioSource, AudioTag, TextSpan } from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { SpeechModel, TranscriptionModel } from './models.ts';\nimport { pushQueue } from './queue.ts';\nimport type { SpeechEvent, SpeechInput } from './speech.ts';\nimport {\n createSpokenState,\n type SpokenPiece,\n spokenPieces,\n type VoiceAgentEvent,\n} from './spoken.ts';\nimport type { ListenOptions, TranscriptionEvent } from './transcription.ts';\n\nexport interface VoiceTurnMeta {\n source: 'voice';\n transcriptionModel: string;\n confidence?: number;\n}\n\nexport interface VoiceCut {\n entryId: string;\n contentIndex: number;\n heardUpTo: number;\n heardMs: number;\n}\n\nexport interface VoiceConversation {\n prompt(\n message: UserMessage,\n meta: VoiceTurnMeta,\n signal: AbortSignal\n ): AsyncIterable<VoiceAgentEvent>;\n truncate(cut: VoiceCut): Promise<void>;\n}\n\nexport type BargeIn = 'interrupt' | 'onWords' | 'ignore' | 'auto';\n\nexport type EchoRecommendation = 'interrupt' | 'onWords' | 'headphones';\n\nexport interface VoiceTurnOptions {\n ears: TranscriptionModel<string>;\n mouth: SpeechModel<string>;\n voice: string;\n speakable?: (event: AssistantMessageEvent) => string | undefined;\n announce?: (call: ToolCall) => string | undefined;\n bargeIn?: BargeIn;\n echoRecommendation?: EchoRecommendation;\n}\n\nexport interface VoiceModels {\n listen(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n ): AsyncIterable<TranscriptionEvent>;\n speak(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n ): AsyncIterable<SpeechEvent>;\n}\n\nexport type VoicePhase =\n | { phase: 'listening' }\n | { phase: 'heard'; text: string }\n | { phase: 'thinking' }\n | { phase: 'tool'; toolName: string; toolCallId: string; args: JsonObject }\n | { phase: 'retrying'; attempt: number; at: number }\n | { phase: 'compacting' }\n | { phase: 'speaking' }\n | { phase: 'idle' };\n\nexport type CaptionEvent =\n | { type: 'user_partial'; turnId: string; segmentId: string; text: string }\n | { type: 'user_final'; turnId: string; segmentId: string; text: string; confidence?: number }\n | { type: 'agent_generated'; turnId: string; utteranceId: string; text: string }\n | { type: 'agent_spoken'; turnId: string; utteranceId: string; upTo: number }\n | { type: 'agent_cut'; turnId: string; utteranceId: string; heardUpTo: number }\n | { type: 'announce'; turnId: string; text: string }\n | { type: 'thinker_progress'; turnId: string; callId: string; text: string }\n | { type: 'thinker_result'; turnId: string; callId: string; summary: string; isError: boolean };\n\nexport interface VoiceSession {\n phases: AsyncIterable<VoicePhase>;\n captions: AsyncIterable<CaptionEvent>;\n done: Promise<void>;\n}\n\ninterface SpokenMap {\n spokenStart: number;\n spokenEnd: number;\n contentIndex: number;\n start: number;\n end: number;\n entryId?: string;\n kind: SpokenPiece['kind'];\n}\n\ninterface ActiveTurn {\n controller: AbortController;\n unlink: () => void;\n done: Promise<void>;\n turnId: string;\n utteranceId: string;\n cut?: VoiceCut;\n}\n\nexport function voiceTurns(\n models: VoiceModels,\n conversation: VoiceConversation,\n mic: AudioSource,\n out: AudioSink,\n options: VoiceTurnOptions,\n signal: AbortSignal\n): VoiceSession {\n const phases = pushQueue<VoicePhase>();\n const captions = pushQueue<CaptionEvent>();\n const session: Session = { models, conversation, mic, out, options, signal, phases, captions };\n const done = runSession(session).finally(() => {\n phases.end();\n captions.end();\n });\n return { phases: phases.iterable, captions: captions.iterable, done };\n}\n\ninterface Session {\n models: VoiceModels;\n conversation: VoiceConversation;\n mic: AudioSource;\n out: AudioSink;\n options: VoiceTurnOptions;\n signal: AbortSignal;\n phases: ReturnType<typeof pushQueue<VoicePhase>>;\n captions: ReturnType<typeof pushQueue<CaptionEvent>>;\n}\n\nasync function runSession(session: Session): Promise<void> {\n const mode = bargeMode(session.options);\n let active: ActiveTurn | undefined;\n let confidence: number | undefined;\n let reserved = 0;\n let openId = '';\n const openTurn = () => {\n if (!openId) {\n reserved += 1;\n openId = String(reserved);\n }\n return openId;\n };\n const stopPlayback = session.out.onPlayback((tag) => {\n if (!active || !('entryId' in tag.span)) return;\n active.cut = {\n entryId: tag.span.entryId,\n contentIndex: tag.span.contentIndex,\n heardUpTo: tag.span.end,\n heardMs: session.out.playedMs,\n };\n });\n const setPhase = phaseSetter(session.phases);\n try {\n if (session.signal.aborted) return;\n setPhase({ phase: 'listening' });\n for await (const event of session.models.listen(session.options.ears, session.mic, {\n signal: session.signal,\n })) {\n if (event.type === 'error') {\n if (event.reason === 'aborted' || session.signal.aborted) return;\n throw new VoiceRequestError(event.reason, event.error);\n }\n const captionId =\n event.type === 'partial' || event.type === 'final' || event.type === 'turn_end'\n ? openTurn()\n : openId;\n confidence = noteUser(session, event, captionId, confidence);\n if (shouldBarge(mode, event)) active = await interrupt(session, active);\n if (event.type !== 'turn_end') continue;\n const turnId = openId || openTurn();\n openId = '';\n if (mode === 'ignore') active = await finishTurn(active);\n else active = await interrupt(session, active);\n setPhase({ phase: 'heard', text: event.text });\n const meta = turnMeta(session.options, confidence);\n confidence = undefined;\n active = startTurn(session, setPhase, turnId, event, meta);\n }\n } finally {\n stopPlayback();\n if (session.signal.aborted && active) await interrupt(session, active);\n else if (active) await active.done;\n setPhase({ phase: 'idle' });\n }\n}\n\nfunction turnMeta(options: VoiceTurnOptions, confidence: number | undefined): VoiceTurnMeta {\n return {\n source: 'voice',\n transcriptionModel: options.ears.id,\n ...(confidence === undefined ? {} : { confidence }),\n };\n}\n\nfunction noteUser(\n session: Session,\n event: TranscriptionEvent,\n turnId: string,\n confidence: number | undefined\n): number | undefined {\n if (event.type === 'partial') {\n session.captions.push({\n type: 'user_partial',\n turnId,\n segmentId: event.segmentId,\n text: event.text,\n });\n }\n if (event.type === 'final') {\n session.captions.push({\n type: 'user_final',\n turnId,\n segmentId: event.segmentId,\n text: event.text,\n ...(event.confidence === undefined ? {} : { confidence: event.confidence }),\n });\n return event.confidence ?? confidence;\n }\n if (event.type === 'turn_end') {\n session.captions.push({ type: 'user_final', turnId, segmentId: 'turn', text: event.text });\n }\n return confidence;\n}\n\nasync function finishTurn(active: ActiveTurn | undefined): Promise<undefined> {\n if (!active) return undefined;\n await active.done;\n return undefined;\n}\n\nfunction startTurn(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n turnId: string,\n event: Extract<TranscriptionEvent, { type: 'turn_end' }>,\n meta: VoiceTurnMeta\n): ActiveTurn {\n const linked = linkSignal(session.signal);\n const active: ActiveTurn = {\n controller: linked.controller,\n unlink: linked.unlink,\n turnId,\n utteranceId: `${turnId}:agent`,\n done: Promise.resolve(),\n };\n active.done = performTurn(session, setPhase, active, event.text, event.at, meta).finally(() => {\n linked.unlink();\n });\n return active;\n}\n\nasync function performTurn(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n active: ActiveTurn,\n text: string,\n at: number,\n meta: VoiceTurnMeta\n): Promise<void> {\n const signal = active.controller.signal;\n try {\n setPhase({ phase: 'thinking' });\n const message: UserMessage = { role: 'user', content: text, timestamp: at };\n const feed = textFeed(signal);\n const maps: SpokenMap[] = [];\n const producing = readAgent(session, setPhase, active, message, meta, feed, maps);\n const audio = session.models.speak(\n session.options.mouth,\n { text: feed.iterable, voice: session.options.voice },\n { signal }\n );\n await Promise.all([producing, pipeSpeech(session, setPhase, active, audio, maps)]);\n if (!signal.aborted) setPhase({ phase: 'listening' });\n } catch (error) {\n if (signal.aborted || session.signal.aborted) return;\n throw error;\n }\n}\n\nasync function readAgent(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n active: ActiveTurn,\n message: UserMessage,\n meta: VoiceTurnMeta,\n feed: TextFeed,\n maps: SpokenMap[]\n): Promise<void> {\n const state = createSpokenState();\n let spokenChars = 0;\n let generated = '';\n try {\n for await (const event of session.conversation.prompt(\n message,\n meta,\n active.controller.signal\n )) {\n if (active.controller.signal.aborted) break;\n notePhase(setPhase, event);\n for (const piece of spokenPieces(event, state, session.options)) {\n spokenChars += piece.text.length;\n generated = rememberPiece(\n session,\n active,\n piece,\n maps,\n spokenChars,\n generated,\n state.entryId\n );\n feed.push(piece.text);\n }\n if (state.entryId) bindEntry(maps, state.entryId);\n }\n } finally {\n feed.end();\n }\n}\n\nfunction rememberPiece(\n session: Session,\n active: ActiveTurn,\n piece: SpokenPiece,\n maps: SpokenMap[],\n spokenEnd: number,\n generated: string,\n entryId: string | undefined\n): string {\n const bound = piece.entryId ?? entryId;\n maps.push({\n spokenStart: spokenEnd - piece.text.length,\n spokenEnd,\n contentIndex: piece.contentIndex,\n start: piece.start,\n end: piece.end,\n kind: piece.kind,\n ...(bound ? { entryId: bound } : {}),\n });\n if (piece.kind === 'announce') {\n session.captions.push({ type: 'announce', turnId: active.turnId, text: piece.text });\n return generated;\n }\n const text = generated + piece.text;\n session.captions.push({\n type: 'agent_generated',\n turnId: active.turnId,\n utteranceId: active.utteranceId,\n text,\n });\n return text;\n}\n\nasync function pipeSpeech(\n session: Session,\n setPhase: (phase: VoicePhase) => void,\n active: ActiveTurn,\n audio: AsyncIterable<SpeechEvent>,\n maps: SpokenMap[]\n): Promise<void> {\n for await (const event of audio) {\n if (active.controller.signal.aborted) return;\n if (event.type === 'error') {\n if (event.reason === 'aborted') return;\n throw new VoiceRequestError(event.reason, event.error);\n }\n if (event.type !== 'audio') continue;\n setPhase({ phase: 'speaking' });\n const span = translateSpan(event.span, maps);\n const tag: AudioTag = {\n span,\n ...(event.alignment ? { alignment: event.alignment } : {}),\n };\n session.out.write(event.chunk, tag);\n }\n}\n\nfunction translateSpan(span: TextSpan, maps: SpokenMap[]): TextSpan {\n if ('entryId' in span) return span;\n const map = answerMap(span, maps);\n if (!map?.entryId) return span;\n const spokenLen = map.spokenEnd - map.spokenStart;\n const sourceLen = map.end - map.start;\n const start = map.start + scale(Math.max(0, span.start - map.spokenStart), spokenLen, sourceLen);\n const end =\n map.start + scale(Math.min(spokenLen, span.end - map.spokenStart), spokenLen, sourceLen);\n return { entryId: map.entryId, contentIndex: map.contentIndex, start, end };\n}\n\nfunction answerMap(span: { start: number; end: number }, maps: SpokenMap[]): SpokenMap | undefined {\n for (let index = maps.length - 1; index >= 0; index -= 1) {\n const item = maps[index];\n if (item?.kind !== 'answer' || !item.entryId) continue;\n if (item.spokenStart < span.end && item.spokenEnd > span.start) return item;\n }\n return undefined;\n}\n\nfunction scale(offset: number, spokenLen: number, sourceLen: number): number {\n if (spokenLen <= 0) return 0;\n return Math.round((offset * sourceLen) / spokenLen);\n}\n\nfunction bindEntry(maps: SpokenMap[], entryId: string): void {\n for (const map of maps) {\n if (map.kind === 'answer' && map.entryId === undefined) map.entryId = entryId;\n }\n}\n\nfunction notePhase(setPhase: (phase: VoicePhase) => void, event: VoiceAgentEvent): void {\n if (event.type === 'run_start' || event.type === 'turn_start') setPhase({ phase: 'thinking' });\n if (event.type === 'compaction_start') setPhase({ phase: 'compacting' });\n if (event.type === 'auto_retry_start' && event.attempt !== undefined && event.at !== undefined) {\n setPhase({ phase: 'retrying', attempt: event.attempt, at: event.at });\n }\n if (event.type === 'tool_execution_start' && event.toolName && event.toolCallId) {\n setPhase({\n phase: 'tool',\n toolName: event.toolName,\n toolCallId: event.toolCallId,\n args: event.args ?? {},\n });\n }\n}\n\nasync function interrupt(session: Session, active: ActiveTurn | undefined): Promise<undefined> {\n if (!active) return undefined;\n active.controller.abort();\n session.out.flush();\n await active.done;\n if (active.cut) {\n session.captions.push({\n type: 'agent_cut',\n turnId: active.turnId,\n utteranceId: active.utteranceId,\n heardUpTo: active.cut.heardUpTo,\n });\n await session.conversation.truncate(active.cut);\n }\n return undefined;\n}\n\nfunction bargeMode(options: VoiceTurnOptions): 'interrupt' | 'onWords' | 'ignore' {\n const mode = options.bargeIn ?? 'interrupt';\n if (mode !== 'auto') return mode;\n if (options.echoRecommendation === 'interrupt') return 'interrupt';\n if (options.echoRecommendation === 'headphones') return 'ignore';\n return 'onWords';\n}\n\nfunction shouldBarge(mode: 'interrupt' | 'onWords' | 'ignore', event: TranscriptionEvent): boolean {\n if (mode === 'ignore') return false;\n if (mode === 'interrupt') return event.type === 'speech_start';\n return event.type === 'partial' && event.text.trim().length > 0;\n}\n\nfunction phaseSetter(phases: Session['phases']): (phase: VoicePhase) => void {\n let previous = '';\n return (phase) => {\n const key = JSON.stringify(phase);\n if (key === previous) return;\n previous = key;\n phases.push(phase);\n };\n}\n\nfunction linkSignal(parent: AbortSignal): { controller: AbortController; unlink: () => void } {\n const controller = new AbortController();\n const abort = () => controller.abort();\n if (parent.aborted) controller.abort();\n else parent.addEventListener('abort', abort);\n return {\n controller,\n unlink: () => parent.removeEventListener('abort', abort),\n };\n}\n\ninterface TextFeed {\n push(text: string): void;\n end(): void;\n iterable: AsyncIterable<string>;\n}\n\nfunction textFeed(signal: AbortSignal): TextFeed {\n const queue: string[] = [];\n let ended = false;\n let wake: (() => void) | undefined;\n const notify = () => {\n const current = wake;\n wake = undefined;\n current?.();\n };\n const onAbort = () => notify();\n signal.addEventListener('abort', onAbort);\n return {\n push(text) {\n if (ended || signal.aborted || text.length === 0) return;\n queue.push(text);\n notify();\n },\n end() {\n ended = true;\n notify();\n },\n iterable: {\n async *[Symbol.asyncIterator]() {\n try {\n while (!signal.aborted) {\n const next = queue.shift();\n if (next !== undefined) {\n yield next;\n continue;\n }\n if (ended) return;\n await new Promise<void>((resolve) => {\n if (queue.length > 0 || ended || signal.aborted) resolve();\n else wake = resolve;\n });\n }\n } finally {\n signal.removeEventListener('abort', onAbort);\n }\n },\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -10,10 +10,13 @@ export { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './
10
10
  export { connectRealtime, listen, registerRealtimeApiProvider, registerSpeechApiProvider, registerTranscriptionApiProvider, speak, transcribe, } from './registry.ts';
11
11
  export type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';
12
12
  export { SpeechEventStream } from './speech.ts';
13
+ export type { VoiceAgentEvent, VoiceMessageChange } from './spoken.ts';
13
14
  export type { BindThinkerOptions, BoundThinker, Thinker, ThinkerCaption, ThinkerPhase, ThinkerUpdate, } from './talker.ts';
14
15
  export { askAgent, bindThinker, talkerTools } from './talker.ts';
15
16
  export type { AudioInput, ListenOptions, Transcript, TranscriptionApiProvider, TranscriptionEvent, TranscriptSegment, } from './transcription.ts';
16
17
  export { TranscriptionEventStream } from './transcription.ts';
18
+ export type { BargeIn, CaptionEvent, EchoRecommendation, VoiceConversation, VoiceCut, VoiceModels, VoicePhase, VoiceSession, VoiceTurnMeta, VoiceTurnOptions, } from './turns.ts';
19
+ export { voiceTurns } from './turns.ts';
17
20
  export type { AudioCost, AudioCounters, AudioUsage, RealtimeAudioCost, SpeechAudioCost, TranscriptionAudioCost, } from './usage.ts';
18
21
  export { addAudioCounters, priceAudio } from './usage.ts';
19
22
  export type { VadHook, VadUpdate } from './vad.ts';
@@ -0,0 +1,7 @@
1
+ import type { RealtimeModel } from '../models.ts';
2
+ import { type AdapterOptions } from './session.ts';
3
+ export declare const gptRealtime2: RealtimeModel<"openai-realtime">;
4
+ export declare const gptRealtime21: RealtimeModel<"openai-realtime">;
5
+ export declare function registerOpenAIRealtime(options?: AdapterOptions): () => void;
6
+ export type { AdapterOptions, VoiceWireFactory } from './session.ts';
7
+ export type { VoiceSocketConstructor, VoiceWire, VoiceWireTarget } from './socket.ts';
@@ -0,0 +1,15 @@
1
+ import type { AudioFormat } from '../audio.ts';
2
+ import type { RealtimeModel } from '../models.ts';
3
+ import type { RealtimeContext, RealtimeOptions, RealtimeSession } from '../realtime.ts';
4
+ import { type VoiceSocketConstructor, type VoiceWire, type VoiceWireTarget } from './socket.ts';
5
+ export type VoiceWireFactory = (target: VoiceWireTarget) => VoiceWire;
6
+ export interface AdapterOptions {
7
+ wire?: VoiceWireFactory;
8
+ socket?: VoiceSocketConstructor;
9
+ replace?: Record<string, string>;
10
+ }
11
+ export interface VendorSessionOptions extends AdapterOptions {
12
+ dialect: 'openai' | 'xai';
13
+ }
14
+ export declare function connectVendorSession(model: RealtimeModel<string>, context: RealtimeContext, options: RealtimeOptions | undefined, vendor: VendorSessionOptions): Promise<RealtimeSession>;
15
+ export declare function pcmFormat(sampleRate: number): AudioFormat;
@@ -0,0 +1,25 @@
1
+ export interface VoiceWireTarget {
2
+ url: string;
3
+ protocols: string[];
4
+ headers: Record<string, string>;
5
+ }
6
+ export interface VoiceWire {
7
+ send(data: string): void;
8
+ close(): void;
9
+ incoming: AsyncIterable<string>;
10
+ }
11
+ export interface VoiceSocket {
12
+ send(data: string): void;
13
+ close(): void;
14
+ addEventListener(type: 'message', listener: (event: {
15
+ data: unknown;
16
+ }) => void): void;
17
+ addEventListener(type: 'close', listener: () => void): void;
18
+ addEventListener(type: 'error', listener: () => void): void;
19
+ }
20
+ export type VoiceSocketInit = {
21
+ protocols: string[];
22
+ headers: Record<string, string>;
23
+ };
24
+ export type VoiceSocketConstructor = new (url: string, init: VoiceSocketInit) => VoiceSocket;
25
+ export declare function openVoiceSocket(target: VoiceWireTarget, Socket?: VoiceSocketConstructor): VoiceWire;
@@ -0,0 +1,6 @@
1
+ export interface PushQueue<T> {
2
+ push(value: T): void;
3
+ end(): void;
4
+ readonly iterable: AsyncIterable<T>;
5
+ }
6
+ export declare function pushQueue<T>(): PushQueue<T>;
@@ -0,0 +1,57 @@
1
+ import type { AssistantMessage, AssistantMessageEvent, JsonObject, TextContent, ThinkingContent, ToolCall } from '@earendil-works/pi-ai';
2
+ export interface VoiceMessageChange {
3
+ type: string;
4
+ contentIndex?: number;
5
+ delta?: string;
6
+ block?: TextContent | ThinkingContent | ToolCall;
7
+ message?: AssistantMessage;
8
+ }
9
+ export interface VoiceEntryRef {
10
+ id: string | number;
11
+ model?: readonly {
12
+ role: string;
13
+ content?: readonly (TextContent | ThinkingContent | ToolCall)[];
14
+ }[];
15
+ }
16
+ export interface VoiceAgentEvent {
17
+ type: string;
18
+ changes?: readonly VoiceMessageChange[];
19
+ message?: {
20
+ role: string;
21
+ content?: unknown;
22
+ };
23
+ entry?: VoiceEntryRef;
24
+ toolName?: string;
25
+ toolCallId?: string;
26
+ args?: JsonObject;
27
+ attempt?: number;
28
+ at?: number;
29
+ }
30
+ export interface SpokenPiece {
31
+ text: string;
32
+ contentIndex: number;
33
+ start: number;
34
+ end: number;
35
+ kind: 'answer' | 'announce';
36
+ entryId?: string;
37
+ }
38
+ interface BlockTrack {
39
+ kind: 'text' | 'thinking' | 'tool';
40
+ text: string;
41
+ sent: number;
42
+ signature?: string;
43
+ ended: boolean;
44
+ }
45
+ export interface SpokenState {
46
+ blocks: Map<number, BlockTrack>;
47
+ entryId?: string;
48
+ }
49
+ export interface SpokenOptions {
50
+ speakable?(event: AssistantMessageEvent): string | undefined;
51
+ announce?(call: ToolCall): string | undefined;
52
+ model?: string;
53
+ }
54
+ export declare function createSpokenState(): SpokenState;
55
+ export declare function isCommentary(signature: string | undefined): boolean;
56
+ export declare function spokenPieces(event: VoiceAgentEvent, state: SpokenState, options?: SpokenOptions): SpokenPiece[];
57
+ export {};
@@ -0,0 +1,107 @@
1
+ import type { AssistantMessageEvent, JsonObject, ProviderRequestOptions, ToolCall, UserMessage } from '@earendil-works/pi-ai';
2
+ import type { AudioSink, AudioSource } from './audio.ts';
3
+ import type { SpeechModel, TranscriptionModel } from './models.ts';
4
+ import type { SpeechEvent, SpeechInput } from './speech.ts';
5
+ import { type VoiceAgentEvent } from './spoken.ts';
6
+ import type { ListenOptions, TranscriptionEvent } from './transcription.ts';
7
+ export interface VoiceTurnMeta {
8
+ source: 'voice';
9
+ transcriptionModel: string;
10
+ confidence?: number;
11
+ }
12
+ export interface VoiceCut {
13
+ entryId: string;
14
+ contentIndex: number;
15
+ heardUpTo: number;
16
+ heardMs: number;
17
+ }
18
+ export interface VoiceConversation {
19
+ prompt(message: UserMessage, meta: VoiceTurnMeta, signal: AbortSignal): AsyncIterable<VoiceAgentEvent>;
20
+ truncate(cut: VoiceCut): Promise<void>;
21
+ }
22
+ export type BargeIn = 'interrupt' | 'onWords' | 'ignore' | 'auto';
23
+ export type EchoRecommendation = 'interrupt' | 'onWords' | 'headphones';
24
+ export interface VoiceTurnOptions {
25
+ ears: TranscriptionModel<string>;
26
+ mouth: SpeechModel<string>;
27
+ voice: string;
28
+ speakable?: (event: AssistantMessageEvent) => string | undefined;
29
+ announce?: (call: ToolCall) => string | undefined;
30
+ bargeIn?: BargeIn;
31
+ echoRecommendation?: EchoRecommendation;
32
+ }
33
+ export interface VoiceModels {
34
+ listen(model: TranscriptionModel<string>, audio: AudioSource, options?: ListenOptions): AsyncIterable<TranscriptionEvent>;
35
+ speak(model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): AsyncIterable<SpeechEvent>;
36
+ }
37
+ export type VoicePhase = {
38
+ phase: 'listening';
39
+ } | {
40
+ phase: 'heard';
41
+ text: string;
42
+ } | {
43
+ phase: 'thinking';
44
+ } | {
45
+ phase: 'tool';
46
+ toolName: string;
47
+ toolCallId: string;
48
+ args: JsonObject;
49
+ } | {
50
+ phase: 'retrying';
51
+ attempt: number;
52
+ at: number;
53
+ } | {
54
+ phase: 'compacting';
55
+ } | {
56
+ phase: 'speaking';
57
+ } | {
58
+ phase: 'idle';
59
+ };
60
+ export type CaptionEvent = {
61
+ type: 'user_partial';
62
+ turnId: string;
63
+ segmentId: string;
64
+ text: string;
65
+ } | {
66
+ type: 'user_final';
67
+ turnId: string;
68
+ segmentId: string;
69
+ text: string;
70
+ confidence?: number;
71
+ } | {
72
+ type: 'agent_generated';
73
+ turnId: string;
74
+ utteranceId: string;
75
+ text: string;
76
+ } | {
77
+ type: 'agent_spoken';
78
+ turnId: string;
79
+ utteranceId: string;
80
+ upTo: number;
81
+ } | {
82
+ type: 'agent_cut';
83
+ turnId: string;
84
+ utteranceId: string;
85
+ heardUpTo: number;
86
+ } | {
87
+ type: 'announce';
88
+ turnId: string;
89
+ text: string;
90
+ } | {
91
+ type: 'thinker_progress';
92
+ turnId: string;
93
+ callId: string;
94
+ text: string;
95
+ } | {
96
+ type: 'thinker_result';
97
+ turnId: string;
98
+ callId: string;
99
+ summary: string;
100
+ isError: boolean;
101
+ };
102
+ export interface VoiceSession {
103
+ phases: AsyncIterable<VoicePhase>;
104
+ captions: AsyncIterable<CaptionEvent>;
105
+ done: Promise<void>;
106
+ }
107
+ export declare function voiceTurns(models: VoiceModels, conversation: VoiceConversation, mic: AudioSource, out: AudioSink, options: VoiceTurnOptions, signal: AbortSignal): VoiceSession;
@@ -0,0 +1,4 @@
1
+ import type { RealtimeModel } from '../models.ts';
2
+ import { type AdapterOptions } from '../openai-realtime/session.ts';
3
+ export declare const grokVoiceLatest: RealtimeModel<'xai-realtime'>;
4
+ export declare function registerXaiRealtime(options?: AdapterOptions): () => void;