@ai-ecoverse/pi-audio 1.7.0 → 1.8.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/index.js +3 -1
- package/dist/index.js.map +2 -2
- package/dist/queue.js +46 -0
- package/dist/queue.js.map +7 -0
- package/dist/spoken.js +197 -0
- package/dist/spoken.js.map +7 -0
- package/dist/turns.js +347 -0
- package/dist/turns.js.map +7 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/queue.d.ts +6 -0
- package/dist/types/spoken.d.ts +57 -0
- package/dist/types/turns.d.ts +107 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { SpeechEventStream } from "./speech.js";
|
|
26
26
|
import { askAgent, bindThinker, talkerTools } from "./talker.js";
|
|
27
27
|
import { TranscriptionEventStream } from "./transcription.js";
|
|
28
|
+
import { voiceTurns } from "./turns.js";
|
|
28
29
|
import { addAudioCounters, priceAudio } from "./usage.js";
|
|
29
30
|
import { energyVad } from "./vad.js";
|
|
30
31
|
export {
|
|
@@ -56,6 +57,7 @@ export {
|
|
|
56
57
|
resolveVoiceAuth,
|
|
57
58
|
speak,
|
|
58
59
|
talkerTools,
|
|
59
|
-
transcribe
|
|
60
|
+
transcribe,
|
|
61
|
+
voiceTurns
|
|
60
62
|
};
|
|
61
63
|
//# 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 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;
|
|
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 { VoiceAgentEvent, VoiceMessageChange } from './spoken.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 BargeIn,\n CaptionEvent,\n EchoRecommendation,\n VoiceConversation,\n VoiceCut,\n VoiceModels,\n VoicePhase,\n VoiceSession,\n VoiceTurnMeta,\n VoiceTurnOptions,\n} from './turns.ts';\nexport { voiceTurns } from './turns.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;AAUlC,SAAS,UAAU,aAAa,mBAAmB;AASnD,SAAS,gCAAgC;AAazC,SAAS,kBAAkB;AAS3B,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/queue.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function pushQueue() {
|
|
2
|
+
const values = [];
|
|
3
|
+
let ended = false;
|
|
4
|
+
let wake;
|
|
5
|
+
const notify = () => {
|
|
6
|
+
const current = wake;
|
|
7
|
+
wake = void 0;
|
|
8
|
+
current?.();
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
push(value) {
|
|
12
|
+
if (ended) return;
|
|
13
|
+
values.push(value);
|
|
14
|
+
notify();
|
|
15
|
+
},
|
|
16
|
+
end() {
|
|
17
|
+
ended = true;
|
|
18
|
+
notify();
|
|
19
|
+
},
|
|
20
|
+
iterable: {
|
|
21
|
+
async *[Symbol.asyncIterator]() {
|
|
22
|
+
let index = 0;
|
|
23
|
+
while (true) {
|
|
24
|
+
if (index < values.length) {
|
|
25
|
+
const value = values[index];
|
|
26
|
+
index += 1;
|
|
27
|
+
if (value !== void 0) yield value;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (ended) return;
|
|
31
|
+
await new Promise((resolve) => {
|
|
32
|
+
wake = () => {
|
|
33
|
+
wake = void 0;
|
|
34
|
+
resolve();
|
|
35
|
+
};
|
|
36
|
+
if (index < values.length || ended) wake();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
pushQueue
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/queue.ts"],
|
|
4
|
+
"sourcesContent": ["export interface PushQueue<T> {\n push(value: T): void;\n end(): void;\n readonly iterable: AsyncIterable<T>;\n}\n\nexport function pushQueue<T>(): PushQueue<T> {\n const values: T[] = [];\n let ended = false;\n let wake: (() => void) | undefined;\n const notify = () => {\n const current = wake;\n wake = undefined;\n current?.();\n };\n return {\n push(value) {\n if (ended) return;\n values.push(value);\n notify();\n },\n end() {\n ended = true;\n notify();\n },\n iterable: {\n async *[Symbol.asyncIterator]() {\n let index = 0;\n while (true) {\n if (index < values.length) {\n const value = values[index];\n index += 1;\n if (value !== undefined) yield value;\n continue;\n }\n if (ended) return;\n await new Promise<void>((resolve) => {\n wake = () => {\n wake = undefined;\n resolve();\n };\n if (index < values.length || ended) wake();\n });\n }\n },\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AAMO,SAAS,YAA6B;AAC3C,QAAM,SAAc,CAAC;AACrB,MAAI,QAAQ;AACZ,MAAI;AACJ,QAAM,SAAS,MAAM;AACnB,UAAM,UAAU;AAChB,WAAO;AACP,cAAU;AAAA,EACZ;AACA,SAAO;AAAA,IACL,KAAK,OAAO;AACV,UAAI,MAAO;AACX,aAAO,KAAK,KAAK;AACjB,aAAO;AAAA,IACT;AAAA,IACA,MAAM;AACJ,cAAQ;AACR,aAAO;AAAA,IACT;AAAA,IACA,UAAU;AAAA,MACR,QAAQ,OAAO,aAAa,IAAI;AAC9B,YAAI,QAAQ;AACZ,eAAO,MAAM;AACX,cAAI,QAAQ,OAAO,QAAQ;AACzB,kBAAM,QAAQ,OAAO,KAAK;AAC1B,qBAAS;AACT,gBAAI,UAAU,OAAW,OAAM;AAC/B;AAAA,UACF;AACA,cAAI,MAAO;AACX,gBAAM,IAAI,QAAc,CAAC,YAAY;AACnC,mBAAO,MAAM;AACX,qBAAO;AACP,sBAAQ;AAAA,YACV;AACA,gBAAI,QAAQ,OAAO,UAAU,MAAO,MAAK;AAAA,UAC3C,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/spoken.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { blankUsage } from "./blank-usage.js";
|
|
2
|
+
function createSpokenState() {
|
|
3
|
+
return { blocks: /* @__PURE__ */ new Map() };
|
|
4
|
+
}
|
|
5
|
+
function isCommentary(signature) {
|
|
6
|
+
if (!signature) return false;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(signature);
|
|
9
|
+
return parsed?.phase === "commentary";
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function spokenPieces(event, state, options = {}) {
|
|
15
|
+
if (event.type === "tool_execution_start") return announceOf(event, options);
|
|
16
|
+
if (event.entry?.id !== void 0) state.entryId = String(event.entry.id);
|
|
17
|
+
if (event.type === "message_end") return messagePieces(event.entry?.model, state, options, true);
|
|
18
|
+
if (event.type === "message_start")
|
|
19
|
+
return messagePieces(assistantContent(event.message), state, options, true);
|
|
20
|
+
if (event.type === "message_update") return changePieces(event.changes ?? [], state, options);
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
function announceOf(event, options) {
|
|
24
|
+
if (!options.announce || !event.toolName || !event.toolCallId) return [];
|
|
25
|
+
const text = options.announce({
|
|
26
|
+
type: "toolCall",
|
|
27
|
+
id: event.toolCallId,
|
|
28
|
+
name: event.toolName,
|
|
29
|
+
arguments: event.args ?? {}
|
|
30
|
+
});
|
|
31
|
+
if (!text) return [];
|
|
32
|
+
return [{ text, contentIndex: -1, start: 0, end: 0, kind: "announce" }];
|
|
33
|
+
}
|
|
34
|
+
function assistantContent(message) {
|
|
35
|
+
if (message?.role !== "assistant") return void 0;
|
|
36
|
+
return Array.isArray(message.content) ? message.content : void 0;
|
|
37
|
+
}
|
|
38
|
+
function messagePieces(model, state, options, close) {
|
|
39
|
+
const content = Array.isArray(model) ? blocksOf(model) : void 0;
|
|
40
|
+
if (!content) return [];
|
|
41
|
+
const pieces = [];
|
|
42
|
+
for (const [contentIndex, block] of content.entries()) {
|
|
43
|
+
const track = applyBlock(state, contentIndex, block);
|
|
44
|
+
const piece = takePiece(track, contentIndex, state, options, close);
|
|
45
|
+
if (piece) pieces.push(piece);
|
|
46
|
+
}
|
|
47
|
+
return pieces;
|
|
48
|
+
}
|
|
49
|
+
function blocksOf(model) {
|
|
50
|
+
const first = model[0];
|
|
51
|
+
if (!first) return void 0;
|
|
52
|
+
if ("role" in first) {
|
|
53
|
+
if (first.role !== "assistant" || !Array.isArray(first.content)) return void 0;
|
|
54
|
+
return first.content;
|
|
55
|
+
}
|
|
56
|
+
return model;
|
|
57
|
+
}
|
|
58
|
+
function changePieces(changes, state, options) {
|
|
59
|
+
const pieces = [];
|
|
60
|
+
for (const change of changes) pieces.push(...applyChange(change, state, options));
|
|
61
|
+
return pieces;
|
|
62
|
+
}
|
|
63
|
+
function applyChange(change, state, options) {
|
|
64
|
+
if (change.type === "message") {
|
|
65
|
+
return messagePieces(change.message ? [change.message] : void 0, state, options, false);
|
|
66
|
+
}
|
|
67
|
+
const contentIndex = change.contentIndex;
|
|
68
|
+
if (contentIndex === void 0) return [];
|
|
69
|
+
if (change.type === "thinking_start" || change.type === "thinking_delta") {
|
|
70
|
+
noteThinking(state, contentIndex, change);
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
if (change.type === "toolcall_start" || change.type === "toolcall_delta") {
|
|
74
|
+
state.blocks.set(contentIndex, { kind: "tool", text: "", sent: 0, ended: true });
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const track = textTrack(state, contentIndex);
|
|
78
|
+
if (change.type === "text_start" || change.type === "block") noteText(track, change.block);
|
|
79
|
+
if (change.type === "text_delta" && change.delta) track.text += change.delta;
|
|
80
|
+
const piece = change.type === "text_delta" && options.speakable ? customPiece(track, contentIndex, change.delta ?? "", state, options, "text_delta") : takePiece(track, contentIndex, state, options, false);
|
|
81
|
+
return piece ? [piece] : [];
|
|
82
|
+
}
|
|
83
|
+
function noteThinking(state, contentIndex, change) {
|
|
84
|
+
const existing = state.blocks.get(contentIndex);
|
|
85
|
+
const text = `${existing?.kind === "thinking" ? existing.text : ""}${change.delta ?? ""}`;
|
|
86
|
+
const started = change.block?.type === "thinking" ? change.block.thinking : text;
|
|
87
|
+
state.blocks.set(contentIndex, {
|
|
88
|
+
kind: "thinking",
|
|
89
|
+
text: change.type === "thinking_start" ? started : text,
|
|
90
|
+
sent: change.type === "thinking_start" ? started.length : text.length,
|
|
91
|
+
ended: true,
|
|
92
|
+
...change.block?.type === "thinking" && change.block.thinkingSignature ? { signature: change.block.thinkingSignature } : {}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function noteText(track, block) {
|
|
96
|
+
if (block?.type !== "text") return;
|
|
97
|
+
track.kind = "text";
|
|
98
|
+
track.text = block.text;
|
|
99
|
+
track.signature = block.textSignature;
|
|
100
|
+
}
|
|
101
|
+
function textTrack(state, contentIndex) {
|
|
102
|
+
const existing = state.blocks.get(contentIndex);
|
|
103
|
+
if (existing) return existing;
|
|
104
|
+
const created = { kind: "text", text: "", sent: 0, ended: false };
|
|
105
|
+
state.blocks.set(contentIndex, created);
|
|
106
|
+
return created;
|
|
107
|
+
}
|
|
108
|
+
function applyBlock(state, contentIndex, block) {
|
|
109
|
+
if (block.type === "thinking") {
|
|
110
|
+
const track2 = {
|
|
111
|
+
kind: "thinking",
|
|
112
|
+
text: block.thinking,
|
|
113
|
+
sent: block.thinking.length,
|
|
114
|
+
ended: true
|
|
115
|
+
};
|
|
116
|
+
state.blocks.set(contentIndex, track2);
|
|
117
|
+
return track2;
|
|
118
|
+
}
|
|
119
|
+
if (block.type === "toolCall") {
|
|
120
|
+
const track2 = { kind: "tool", text: "", sent: 0, ended: true };
|
|
121
|
+
state.blocks.set(contentIndex, track2);
|
|
122
|
+
return track2;
|
|
123
|
+
}
|
|
124
|
+
const existing = state.blocks.get(contentIndex);
|
|
125
|
+
const sent = existing?.kind === "text" ? existing.sent : 0;
|
|
126
|
+
let ended = false;
|
|
127
|
+
if (existing?.kind === "text") ended = existing.ended;
|
|
128
|
+
const track = {
|
|
129
|
+
kind: "text",
|
|
130
|
+
text: block.text,
|
|
131
|
+
sent,
|
|
132
|
+
ended,
|
|
133
|
+
...block.textSignature ? { signature: block.textSignature } : {}
|
|
134
|
+
};
|
|
135
|
+
state.blocks.set(contentIndex, track);
|
|
136
|
+
return track;
|
|
137
|
+
}
|
|
138
|
+
function takePiece(track, contentIndex, state, options, close) {
|
|
139
|
+
if (track.kind !== "text") return void 0;
|
|
140
|
+
if (options.speakable) {
|
|
141
|
+
if (!close || track.ended) return void 0;
|
|
142
|
+
track.ended = true;
|
|
143
|
+
return customPiece(track, contentIndex, track.text, state, options, "text_end");
|
|
144
|
+
}
|
|
145
|
+
if (isCommentary(track.signature)) {
|
|
146
|
+
track.sent = track.text.length;
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
if (track.sent >= track.text.length) return void 0;
|
|
150
|
+
const start = track.sent;
|
|
151
|
+
const text = track.text.slice(start);
|
|
152
|
+
track.sent = track.text.length;
|
|
153
|
+
if (!text) return void 0;
|
|
154
|
+
return { text, contentIndex, start, end: track.sent, kind: "answer", entryId: state.entryId };
|
|
155
|
+
}
|
|
156
|
+
function customPiece(track, contentIndex, delta, state, options, type) {
|
|
157
|
+
if (!options.speakable) return void 0;
|
|
158
|
+
const partial = partialFrom(state, options.model ?? "voice");
|
|
159
|
+
const event = type === "text_delta" ? { type: "text_delta", contentIndex, delta, partial } : { type: "text_end", contentIndex, content: track.text, partial };
|
|
160
|
+
const text = options.speakable(event);
|
|
161
|
+
const start = track.sent;
|
|
162
|
+
track.sent = track.text.length;
|
|
163
|
+
track.ended = type === "text_end";
|
|
164
|
+
if (!text) return void 0;
|
|
165
|
+
return { text, contentIndex, start, end: track.sent, kind: "answer", entryId: state.entryId };
|
|
166
|
+
}
|
|
167
|
+
function partialFrom(state, model) {
|
|
168
|
+
const max = Math.max(-1, ...state.blocks.keys());
|
|
169
|
+
const content = [];
|
|
170
|
+
for (let index = 0; index <= max; index += 1) content.push(partialBlock(state.blocks.get(index)));
|
|
171
|
+
return {
|
|
172
|
+
role: "assistant",
|
|
173
|
+
content,
|
|
174
|
+
api: "pi-audio",
|
|
175
|
+
provider: "pi-audio",
|
|
176
|
+
model,
|
|
177
|
+
usage: blankUsage(),
|
|
178
|
+
stopReason: "pending",
|
|
179
|
+
timestamp: 0
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function partialBlock(track) {
|
|
183
|
+
if (!track || track.kind === "thinking") return { type: "thinking", thinking: track?.text ?? "" };
|
|
184
|
+
if (track.kind === "tool")
|
|
185
|
+
return { type: "toolCall", id: "pending", name: "pending", arguments: {} };
|
|
186
|
+
return {
|
|
187
|
+
type: "text",
|
|
188
|
+
text: track.text,
|
|
189
|
+
...track.signature ? { textSignature: track.signature } : {}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
createSpokenState,
|
|
194
|
+
isCommentary,
|
|
195
|
+
spokenPieces
|
|
196
|
+
};
|
|
197
|
+
//# sourceMappingURL=spoken.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/spoken.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n AssistantMessage,\n AssistantMessageEvent,\n JsonObject,\n TextContent,\n ThinkingContent,\n ToolCall,\n} from '@earendil-works/pi-ai';\nimport { blankUsage } from './blank-usage.ts';\n\nexport interface VoiceMessageChange {\n type: string;\n contentIndex?: number;\n delta?: string;\n block?: TextContent | ThinkingContent | ToolCall;\n message?: AssistantMessage;\n}\n\nexport interface VoiceEntryRef {\n id: string | number;\n model?: readonly {\n role: string;\n content?: readonly (TextContent | ThinkingContent | ToolCall)[];\n }[];\n}\n\nexport interface VoiceAgentEvent {\n type: string;\n changes?: readonly VoiceMessageChange[];\n message?: { role: string; content?: unknown };\n entry?: VoiceEntryRef;\n toolName?: string;\n toolCallId?: string;\n args?: JsonObject;\n attempt?: number;\n at?: number;\n}\n\nexport interface SpokenPiece {\n text: string;\n contentIndex: number;\n start: number;\n end: number;\n kind: 'answer' | 'announce';\n entryId?: string;\n}\n\ninterface BlockTrack {\n kind: 'text' | 'thinking' | 'tool';\n text: string;\n sent: number;\n signature?: string;\n ended: boolean;\n}\n\nexport interface SpokenState {\n blocks: Map<number, BlockTrack>;\n entryId?: string;\n}\n\nexport interface SpokenOptions {\n speakable?(event: AssistantMessageEvent): string | undefined;\n announce?(call: ToolCall): string | undefined;\n model?: string;\n}\n\ntype Block = TextContent | ThinkingContent | ToolCall;\n\nexport function createSpokenState(): SpokenState {\n return { blocks: new Map() };\n}\n\nexport function isCommentary(signature: string | undefined): boolean {\n if (!signature) return false;\n try {\n const parsed = JSON.parse(signature) as { phase?: unknown } | null;\n return parsed?.phase === 'commentary';\n } catch {\n return false;\n }\n}\n\nexport function spokenPieces(\n event: VoiceAgentEvent,\n state: SpokenState,\n options: SpokenOptions = {}\n): SpokenPiece[] {\n if (event.type === 'tool_execution_start') return announceOf(event, options);\n if (event.entry?.id !== undefined) state.entryId = String(event.entry.id);\n if (event.type === 'message_end') return messagePieces(event.entry?.model, state, options, true);\n if (event.type === 'message_start')\n return messagePieces(assistantContent(event.message), state, options, true);\n if (event.type === 'message_update') return changePieces(event.changes ?? [], state, options);\n return [];\n}\n\nfunction announceOf(event: VoiceAgentEvent, options: SpokenOptions): SpokenPiece[] {\n if (!options.announce || !event.toolName || !event.toolCallId) return [];\n const text = options.announce({\n type: 'toolCall',\n id: event.toolCallId,\n name: event.toolName,\n arguments: event.args ?? {},\n });\n if (!text) return [];\n return [{ text, contentIndex: -1, start: 0, end: 0, kind: 'announce' }];\n}\n\nfunction assistantContent(message: VoiceAgentEvent['message']): readonly Block[] | undefined {\n if (message?.role !== 'assistant') return undefined;\n return Array.isArray(message.content) ? (message.content as readonly Block[]) : undefined;\n}\n\nfunction messagePieces(\n model: VoiceEntryRef['model'] | readonly Block[] | undefined,\n state: SpokenState,\n options: SpokenOptions,\n close: boolean\n): SpokenPiece[] {\n const content = Array.isArray(model) ? blocksOf(model) : undefined;\n if (!content) return [];\n const pieces: SpokenPiece[] = [];\n for (const [contentIndex, block] of content.entries()) {\n const track = applyBlock(state, contentIndex, block);\n const piece = takePiece(track, contentIndex, state, options, close);\n if (piece) pieces.push(piece);\n }\n return pieces;\n}\n\nfunction blocksOf(\n model: readonly Block[] | NonNullable<VoiceEntryRef['model']>\n): readonly Block[] | undefined {\n const first = model[0] as { role?: string; content?: unknown } | Block | undefined;\n if (!first) return undefined;\n if ('role' in first) {\n if (first.role !== 'assistant' || !Array.isArray(first.content)) return undefined;\n return first.content as readonly Block[];\n }\n return model as readonly Block[];\n}\n\nfunction changePieces(\n changes: readonly VoiceMessageChange[],\n state: SpokenState,\n options: SpokenOptions\n): SpokenPiece[] {\n const pieces: SpokenPiece[] = [];\n for (const change of changes) pieces.push(...applyChange(change, state, options));\n return pieces;\n}\n\nfunction applyChange(\n change: VoiceMessageChange,\n state: SpokenState,\n options: SpokenOptions\n): SpokenPiece[] {\n if (change.type === 'message') {\n return messagePieces(change.message ? [change.message] : undefined, state, options, false);\n }\n const contentIndex = change.contentIndex;\n if (contentIndex === undefined) return [];\n if (change.type === 'thinking_start' || change.type === 'thinking_delta') {\n noteThinking(state, contentIndex, change);\n return [];\n }\n if (change.type === 'toolcall_start' || change.type === 'toolcall_delta') {\n state.blocks.set(contentIndex, { kind: 'tool', text: '', sent: 0, ended: true });\n return [];\n }\n const track = textTrack(state, contentIndex);\n if (change.type === 'text_start' || change.type === 'block') noteText(track, change.block);\n if (change.type === 'text_delta' && change.delta) track.text += change.delta;\n const piece =\n change.type === 'text_delta' && options.speakable\n ? customPiece(track, contentIndex, change.delta ?? '', state, options, 'text_delta')\n : takePiece(track, contentIndex, state, options, false);\n return piece ? [piece] : [];\n}\n\nfunction noteThinking(state: SpokenState, contentIndex: number, change: VoiceMessageChange): void {\n const existing = state.blocks.get(contentIndex);\n const text = `${existing?.kind === 'thinking' ? existing.text : ''}${change.delta ?? ''}`;\n const started = change.block?.type === 'thinking' ? change.block.thinking : text;\n state.blocks.set(contentIndex, {\n kind: 'thinking',\n text: change.type === 'thinking_start' ? started : text,\n sent: change.type === 'thinking_start' ? started.length : text.length,\n ended: true,\n ...(change.block?.type === 'thinking' && change.block.thinkingSignature\n ? { signature: change.block.thinkingSignature }\n : {}),\n });\n}\n\nfunction noteText(track: BlockTrack, block: VoiceMessageChange['block']): void {\n if (block?.type !== 'text') return;\n track.kind = 'text';\n track.text = block.text;\n track.signature = block.textSignature;\n}\n\nfunction textTrack(state: SpokenState, contentIndex: number): BlockTrack {\n const existing = state.blocks.get(contentIndex);\n if (existing) return existing;\n const created: BlockTrack = { kind: 'text', text: '', sent: 0, ended: false };\n state.blocks.set(contentIndex, created);\n return created;\n}\n\nfunction applyBlock(state: SpokenState, contentIndex: number, block: Block): BlockTrack {\n if (block.type === 'thinking') {\n const track: BlockTrack = {\n kind: 'thinking',\n text: block.thinking,\n sent: block.thinking.length,\n ended: true,\n };\n state.blocks.set(contentIndex, track);\n return track;\n }\n if (block.type === 'toolCall') {\n const track: BlockTrack = { kind: 'tool', text: '', sent: 0, ended: true };\n state.blocks.set(contentIndex, track);\n return track;\n }\n const existing = state.blocks.get(contentIndex);\n const sent = existing?.kind === 'text' ? existing.sent : 0;\n let ended = false;\n if (existing?.kind === 'text') ended = existing.ended;\n const track: BlockTrack = {\n kind: 'text',\n text: block.text,\n sent,\n ended,\n ...(block.textSignature ? { signature: block.textSignature } : {}),\n };\n state.blocks.set(contentIndex, track);\n return track;\n}\n\nfunction takePiece(\n track: BlockTrack,\n contentIndex: number,\n state: SpokenState,\n options: SpokenOptions,\n close: boolean\n): SpokenPiece | undefined {\n if (track.kind !== 'text') return undefined;\n if (options.speakable) {\n if (!close || track.ended) return undefined;\n track.ended = true;\n return customPiece(track, contentIndex, track.text, state, options, 'text_end');\n }\n if (isCommentary(track.signature)) {\n track.sent = track.text.length;\n return undefined;\n }\n if (track.sent >= track.text.length) return undefined;\n const start = track.sent;\n const text = track.text.slice(start);\n track.sent = track.text.length;\n if (!text) return undefined;\n return { text, contentIndex, start, end: track.sent, kind: 'answer', entryId: state.entryId };\n}\n\nfunction customPiece(\n track: BlockTrack,\n contentIndex: number,\n delta: string,\n state: SpokenState,\n options: SpokenOptions,\n type: 'text_delta' | 'text_end'\n): SpokenPiece | undefined {\n if (!options.speakable) return undefined;\n const partial = partialFrom(state, options.model ?? 'voice');\n const event: AssistantMessageEvent =\n type === 'text_delta'\n ? { type: 'text_delta', contentIndex, delta, partial }\n : { type: 'text_end', contentIndex, content: track.text, partial };\n const text = options.speakable(event);\n const start = track.sent;\n track.sent = track.text.length;\n track.ended = type === 'text_end';\n if (!text) return undefined;\n return { text, contentIndex, start, end: track.sent, kind: 'answer', entryId: state.entryId };\n}\n\nfunction partialFrom(state: SpokenState, model: string): AssistantMessage {\n const max = Math.max(-1, ...state.blocks.keys());\n const content: Block[] = [];\n for (let index = 0; index <= max; index += 1) content.push(partialBlock(state.blocks.get(index)));\n return {\n role: 'assistant',\n content,\n api: 'pi-audio',\n provider: 'pi-audio',\n model,\n usage: blankUsage(),\n stopReason: 'pending',\n timestamp: 0,\n };\n}\n\nfunction partialBlock(track: BlockTrack | undefined): Block {\n if (!track || track.kind === 'thinking') return { type: 'thinking', thinking: track?.text ?? '' };\n if (track.kind === 'tool')\n return { type: 'toolCall', id: 'pending', name: 'pending', arguments: {} };\n return {\n type: 'text',\n text: track.text,\n ...(track.signature ? { textSignature: track.signature } : {}),\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["track"]
|
|
7
|
+
}
|
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
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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,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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"release": "semantic-release"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@ai-ecoverse/slicc-shared-web": "1.21.
|
|
60
|
+
"@ai-ecoverse/slicc-shared-web": "1.21.1",
|
|
61
61
|
"@biomejs/biome": "2.5.15",
|
|
62
62
|
"@earendil-works/pi-ai": "1.1.0",
|
|
63
63
|
"@types/node": "24.19.1",
|