@ai-ecoverse/pi-audio 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/elevenlabs-agents/events.js +172 -0
- package/dist/elevenlabs-agents/events.js.map +7 -0
- package/dist/elevenlabs-agents/index.js +58 -0
- package/dist/elevenlabs-agents/index.js.map +7 -0
- package/dist/elevenlabs-agents/provision.js +167 -0
- package/dist/elevenlabs-agents/provision.js.map +7 -0
- package/dist/elevenlabs-agents/session.js +272 -0
- package/dist/elevenlabs-agents/session.js.map +7 -0
- package/dist/hosted-agent.js +35 -0
- package/dist/hosted-agent.js.map +7 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/soundscape.js +104 -0
- package/dist/soundscape.js.map +7 -0
- package/dist/turns.js +28 -4
- package/dist/turns.js.map +2 -2
- package/dist/types/elevenlabs-agents/events.d.ts +13 -0
- package/dist/types/elevenlabs-agents/index.d.ts +9 -0
- package/dist/types/elevenlabs-agents/provision.d.ts +11 -0
- package/dist/types/elevenlabs-agents/session.d.ts +16 -0
- package/dist/types/hosted-agent.d.ts +23 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/soundscape.d.ts +47 -0
- package/dist/types/turns.d.ts +3 -1
- package/package.json +5 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { encodeAudioBase64 } from "../convert.js";
|
|
2
|
+
import { VoiceRequestError } from "../errors.js";
|
|
3
|
+
import {
|
|
4
|
+
openVoiceSocket
|
|
5
|
+
} from "../openai-realtime/socket.js";
|
|
6
|
+
import { pushQueue } from "../queue.js";
|
|
7
|
+
import { planTruncation, RealtimeLedger, realtimeSocket } from "../realtime.js";
|
|
8
|
+
import { mintRealtimeToken } from "../tokens.js";
|
|
9
|
+
import { priceAudio } from "../usage.js";
|
|
10
|
+
import { ingestElevenLabs } from "./events.js";
|
|
11
|
+
import { provisionAgent } from "./provision.js";
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function resultText(result) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
for (const part of result.content) {
|
|
18
|
+
if (part.type === "text") parts.push(part.text);
|
|
19
|
+
}
|
|
20
|
+
return parts.join("\n");
|
|
21
|
+
}
|
|
22
|
+
function injectMessage(value, delivery) {
|
|
23
|
+
if (delivery === "silent") {
|
|
24
|
+
return { type: "contextual_update", text: value, context_id: "pi-audio" };
|
|
25
|
+
}
|
|
26
|
+
const text = delivery === "when_idle" ? `[agent update] ${value}` : value;
|
|
27
|
+
return { type: "user_message", text };
|
|
28
|
+
}
|
|
29
|
+
function toolResult(callId, result) {
|
|
30
|
+
return {
|
|
31
|
+
type: "client_tool_result",
|
|
32
|
+
tool_call_id: callId,
|
|
33
|
+
result: resultText(result),
|
|
34
|
+
is_error: result.isError
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function talkerLlm(model) {
|
|
38
|
+
const cascade = model.cascade;
|
|
39
|
+
if (cascade === void 0) return "gemini-2.5-flash";
|
|
40
|
+
return cascade.llm;
|
|
41
|
+
}
|
|
42
|
+
function specFromContext(model, context) {
|
|
43
|
+
return {
|
|
44
|
+
name: model.name,
|
|
45
|
+
instructions: context.instructions,
|
|
46
|
+
tools: context.tools,
|
|
47
|
+
voice: context.voice,
|
|
48
|
+
llm: talkerLlm(model)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function hasKey(options) {
|
|
52
|
+
return typeof options?.apiKey === "string" && options.apiKey.length > 0;
|
|
53
|
+
}
|
|
54
|
+
function agentFromToken(value) {
|
|
55
|
+
if (!value.startsWith("ws")) return void 0;
|
|
56
|
+
const id = new URL(value).searchParams.get("agent_id");
|
|
57
|
+
return id === null ? void 0 : id;
|
|
58
|
+
}
|
|
59
|
+
function conversationUrl(model, agentId, token) {
|
|
60
|
+
const url = new URL(model.baseUrl);
|
|
61
|
+
url.searchParams.set("agent_id", agentId);
|
|
62
|
+
if (token !== void 0) url.searchParams.set("token", token);
|
|
63
|
+
return url.toString();
|
|
64
|
+
}
|
|
65
|
+
function socketHeaders(options) {
|
|
66
|
+
const headers = {};
|
|
67
|
+
const extra = options?.headers;
|
|
68
|
+
if (extra === void 0) return headers;
|
|
69
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
70
|
+
if (value === null) delete headers[key];
|
|
71
|
+
else headers[key] = value;
|
|
72
|
+
}
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
function placeholder(agentId) {
|
|
76
|
+
return { agentId, specHash: "", publishedAt: 0 };
|
|
77
|
+
}
|
|
78
|
+
async function ensureAgent(model, context, options, settings) {
|
|
79
|
+
const token = options?.token?.value;
|
|
80
|
+
if (typeof token === "string" && token.length > 0) {
|
|
81
|
+
return placeholder(agentFromToken(token) ?? settings.agentId ?? model.id);
|
|
82
|
+
}
|
|
83
|
+
if (!hasKey(options)) return placeholder(settings.agentId ?? model.id);
|
|
84
|
+
const provision = {
|
|
85
|
+
...options,
|
|
86
|
+
agentId: settings.agentId,
|
|
87
|
+
cache: settings.cache,
|
|
88
|
+
now: settings.now
|
|
89
|
+
};
|
|
90
|
+
return provisionAgent(model, specFromContext(model, context), provision);
|
|
91
|
+
}
|
|
92
|
+
async function socketUrl(model, agentId, context, options) {
|
|
93
|
+
const token = options?.token?.value;
|
|
94
|
+
if (typeof token === "string" && token.startsWith("ws")) return token;
|
|
95
|
+
if (typeof token === "string" && token.length > 0) return conversationUrl(model, agentId, token);
|
|
96
|
+
if (!hasKey(options)) return conversationUrl(model, agentId);
|
|
97
|
+
const minted = await mintRealtimeToken(
|
|
98
|
+
model,
|
|
99
|
+
{
|
|
100
|
+
agentId,
|
|
101
|
+
instructions: context.instructions,
|
|
102
|
+
voice: context.voice,
|
|
103
|
+
tools: context.tools
|
|
104
|
+
},
|
|
105
|
+
options
|
|
106
|
+
);
|
|
107
|
+
return minted.value;
|
|
108
|
+
}
|
|
109
|
+
function initiation(context, firstMessage) {
|
|
110
|
+
const agent = { prompt: { prompt: context.instructions } };
|
|
111
|
+
if (firstMessage !== void 0) agent.first_message = firstMessage;
|
|
112
|
+
return {
|
|
113
|
+
type: "conversation_initiation_client_data",
|
|
114
|
+
conversation_config_override: { agent, tts: { voice_id: context.voice } },
|
|
115
|
+
dynamic_variables: {},
|
|
116
|
+
custom_llm_extra_body: {}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
class ElevenLabsSession {
|
|
120
|
+
model;
|
|
121
|
+
#context;
|
|
122
|
+
#wire;
|
|
123
|
+
#now;
|
|
124
|
+
#started;
|
|
125
|
+
#ledger = new RealtimeLedger();
|
|
126
|
+
#events = pushQueue();
|
|
127
|
+
#turn = { vad: false, dropAudio: false, lastInterruptId: -1 };
|
|
128
|
+
#closed = false;
|
|
129
|
+
#usage;
|
|
130
|
+
constructor(model, context, wire, now) {
|
|
131
|
+
this.model = model;
|
|
132
|
+
this.#context = context;
|
|
133
|
+
this.#wire = wire;
|
|
134
|
+
this.#now = now;
|
|
135
|
+
this.#started = now();
|
|
136
|
+
}
|
|
137
|
+
start(firstMessage) {
|
|
138
|
+
this.#send(initiation(this.#context, firstMessage));
|
|
139
|
+
void this.#read();
|
|
140
|
+
}
|
|
141
|
+
#assertOpen() {
|
|
142
|
+
if (this.#closed) throw new VoiceRequestError("error", "session is closed");
|
|
143
|
+
}
|
|
144
|
+
#send(event) {
|
|
145
|
+
this.#wire.send(JSON.stringify(event));
|
|
146
|
+
}
|
|
147
|
+
#emit(event) {
|
|
148
|
+
if (this.#closed) return;
|
|
149
|
+
this.#ledger.observe(event);
|
|
150
|
+
this.#events.push(event);
|
|
151
|
+
}
|
|
152
|
+
appendAudio(chunk) {
|
|
153
|
+
this.#assertOpen();
|
|
154
|
+
this.#ledger.addInput(chunk);
|
|
155
|
+
this.#send({ user_audio_chunk: encodeAudioBase64(chunk.data) });
|
|
156
|
+
}
|
|
157
|
+
sendText(value) {
|
|
158
|
+
this.#assertOpen();
|
|
159
|
+
this.#ledger.addText();
|
|
160
|
+
this.#send({ type: "user_message", text: value });
|
|
161
|
+
}
|
|
162
|
+
inject(value, delivery) {
|
|
163
|
+
this.#assertOpen();
|
|
164
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
165
|
+
this.#assertOpen();
|
|
166
|
+
this.#ledger.addText();
|
|
167
|
+
this.#send(injectMessage(value, delivery));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
respond(callId, result, delivery) {
|
|
171
|
+
this.#assertOpen();
|
|
172
|
+
const send = () => {
|
|
173
|
+
this.#send(toolResult(callId, result));
|
|
174
|
+
};
|
|
175
|
+
if (delivery === void 0) {
|
|
176
|
+
send();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
180
|
+
this.#assertOpen();
|
|
181
|
+
send();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
interrupt(heard) {
|
|
185
|
+
this.#assertOpen();
|
|
186
|
+
planTruncation(this.model.truncation, heard);
|
|
187
|
+
this.#turn.dropAudio = true;
|
|
188
|
+
this.#ledger.settle();
|
|
189
|
+
}
|
|
190
|
+
update(patch) {
|
|
191
|
+
this.#assertOpen();
|
|
192
|
+
if (patch.tools !== void 0 && this.model.liveToolUpdate === false) {
|
|
193
|
+
throw new VoiceRequestError("error", "live tool update is unsupported");
|
|
194
|
+
}
|
|
195
|
+
if (patch.tools !== void 0) this.#context.tools = patch.tools;
|
|
196
|
+
if (patch.instructions !== void 0) {
|
|
197
|
+
this.#context.instructions = patch.instructions;
|
|
198
|
+
this.#send({
|
|
199
|
+
type: "contextual_update",
|
|
200
|
+
text: patch.instructions,
|
|
201
|
+
context_id: "pi-audio"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (patch.voice !== void 0) this.#context.voice = patch.voice;
|
|
205
|
+
}
|
|
206
|
+
async close() {
|
|
207
|
+
if (this.#usage !== void 0) return { usage: this.#usage };
|
|
208
|
+
this.#ledger.settle();
|
|
209
|
+
this.#closed = true;
|
|
210
|
+
this.#wire.close();
|
|
211
|
+
this.#events.end();
|
|
212
|
+
const inputSeconds = Math.max(0, this.#now() - this.#started) / 1e3;
|
|
213
|
+
this.#usage = priceAudio(this.model.audioCost, {
|
|
214
|
+
inputSeconds,
|
|
215
|
+
outputSeconds: this.#ledger.outputSeconds,
|
|
216
|
+
textMessages: this.#ledger.textMessages
|
|
217
|
+
});
|
|
218
|
+
return { usage: this.#usage };
|
|
219
|
+
}
|
|
220
|
+
[Symbol.asyncIterator]() {
|
|
221
|
+
return this.#events.iterable[Symbol.asyncIterator]();
|
|
222
|
+
}
|
|
223
|
+
#onRaw(raw) {
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(raw);
|
|
227
|
+
} catch {
|
|
228
|
+
this.#emit({ type: "error", error: "invalid realtime event", fatal: false });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!isRecord(parsed)) return;
|
|
232
|
+
const ingested = ingestElevenLabs(this.#turn, parsed, this.model.outputFormat);
|
|
233
|
+
for (const reply of ingested.replies) this.#send(reply);
|
|
234
|
+
for (const event of ingested.events) this.#emit(event);
|
|
235
|
+
}
|
|
236
|
+
#fail(error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
this.#emit({ type: "error", error: message, fatal: true });
|
|
239
|
+
}
|
|
240
|
+
async #read() {
|
|
241
|
+
try {
|
|
242
|
+
for await (const raw of this.#wire.incoming) this.#onRaw(raw);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.#fail(error);
|
|
245
|
+
} finally {
|
|
246
|
+
this.#closed = true;
|
|
247
|
+
this.#events.end();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function connectElevenLabs(model, context, options, settings = {}) {
|
|
252
|
+
if (options?.signal?.aborted === true) throw new VoiceRequestError("aborted", "session aborted");
|
|
253
|
+
const agent = await ensureAgent(model, context, options, settings);
|
|
254
|
+
const url = await socketUrl(model, agent.agentId, context, options);
|
|
255
|
+
const routed = realtimeSocket(url, options, []);
|
|
256
|
+
const target = {
|
|
257
|
+
url: routed.url,
|
|
258
|
+
protocols: routed.protocols,
|
|
259
|
+
headers: socketHeaders(options)
|
|
260
|
+
};
|
|
261
|
+
const open = settings.wire !== void 0 ? settings.wire : (wireTarget) => openVoiceSocket(wireTarget, settings.socket);
|
|
262
|
+
const now = settings.now !== void 0 ? settings.now : Date.now;
|
|
263
|
+
const session = new ElevenLabsSession(model, context, open(target), now);
|
|
264
|
+
session.start(settings.firstMessage);
|
|
265
|
+
return session;
|
|
266
|
+
}
|
|
267
|
+
export {
|
|
268
|
+
connectElevenLabs,
|
|
269
|
+
specFromContext,
|
|
270
|
+
talkerLlm
|
|
271
|
+
};
|
|
272
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/elevenlabs-agents/session.ts"],
|
|
4
|
+
"sourcesContent": ["import type { JsonObject, ToolResultMessage, Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk } from '../audio.ts';\nimport { encodeAudioBase64 } from '../convert.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';\nimport type { Delivery, RealtimeModel } from '../models.ts';\nimport {\n openVoiceSocket,\n type VoiceSocketConstructor,\n type VoiceWire,\n type VoiceWireTarget,\n} from '../openai-realtime/socket.ts';\nimport { pushQueue } from '../queue.ts';\nimport type {\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n} from '../realtime.ts';\nimport { planTruncation, RealtimeLedger, realtimeSocket } from '../realtime.ts';\nimport { mintRealtimeToken } from '../tokens.ts';\nimport { priceAudio } from '../usage.ts';\nimport { ingestElevenLabs, type TalkTurn } from './events.ts';\nimport { type ProvisionOptions, provisionAgent } from './provision.ts';\n\nexport type VoiceWireFactory = (target: VoiceWireTarget) => VoiceWire;\n\nexport interface ElevenLabsOptions {\n wire?: VoiceWireFactory;\n socket?: VoiceSocketConstructor;\n cache?: HostedCache;\n agentId?: string;\n now?: () => number;\n firstMessage?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction resultText(result: ToolResultMessage): string {\n const parts: string[] = [];\n for (const part of result.content) {\n if (part.type === 'text') parts.push(part.text);\n }\n return parts.join('\\n');\n}\n\nfunction injectMessage(value: string, delivery: Delivery): JsonObject {\n if (delivery === 'silent') {\n return { type: 'contextual_update', text: value, context_id: 'pi-audio' };\n }\n const text = delivery === 'when_idle' ? `[agent update] ${value}` : value;\n return { type: 'user_message', text };\n}\n\nfunction toolResult(callId: string, result: ToolResultMessage): JsonObject {\n return {\n type: 'client_tool_result',\n tool_call_id: callId,\n result: resultText(result),\n is_error: result.isError,\n };\n}\n\nexport function talkerLlm(model: RealtimeModel<string>): string {\n const cascade = model.cascade;\n if (cascade === undefined) return 'gemini-2.5-flash';\n return cascade.llm;\n}\n\nexport function specFromContext(\n model: RealtimeModel<string>,\n context: RealtimeContext\n): HostedAgentSpec {\n return {\n name: model.name,\n instructions: context.instructions,\n tools: context.tools,\n voice: context.voice,\n llm: talkerLlm(model),\n };\n}\n\nfunction hasKey(options: RealtimeOptions | undefined): boolean {\n return typeof options?.apiKey === 'string' && options.apiKey.length > 0;\n}\n\nfunction agentFromToken(value: string): string | undefined {\n if (!value.startsWith('ws')) return undefined;\n const id = new URL(value).searchParams.get('agent_id');\n return id === null ? undefined : id;\n}\n\nfunction conversationUrl(model: RealtimeModel<string>, agentId: string, token?: string): string {\n const url = new URL(model.baseUrl);\n url.searchParams.set('agent_id', agentId);\n if (token !== undefined) url.searchParams.set('token', token);\n return url.toString();\n}\n\nfunction socketHeaders(options: RealtimeOptions | undefined): Record<string, string> {\n const headers: Record<string, string> = {};\n const extra = options?.headers;\n if (extra === undefined) return headers;\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n return headers;\n}\n\nfunction placeholder(agentId: string): HostedAgent {\n return { agentId, specHash: '', publishedAt: 0 };\n}\n\nasync function ensureAgent(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: ElevenLabsOptions\n): Promise<HostedAgent> {\n const token = options?.token?.value;\n if (typeof token === 'string' && token.length > 0) {\n return placeholder(agentFromToken(token) ?? settings.agentId ?? model.id);\n }\n if (!hasKey(options)) return placeholder(settings.agentId ?? model.id);\n const provision: ProvisionOptions = {\n ...options,\n agentId: settings.agentId,\n cache: settings.cache,\n now: settings.now,\n };\n return provisionAgent(model, specFromContext(model, context), provision);\n}\n\nasync function socketUrl(\n model: RealtimeModel<string>,\n agentId: string,\n context: RealtimeContext,\n options: RealtimeOptions | undefined\n): Promise<string> {\n const token = options?.token?.value;\n if (typeof token === 'string' && token.startsWith('ws')) return token;\n if (typeof token === 'string' && token.length > 0) return conversationUrl(model, agentId, token);\n if (!hasKey(options)) return conversationUrl(model, agentId);\n const minted = await mintRealtimeToken(\n model,\n {\n agentId,\n instructions: context.instructions,\n voice: context.voice,\n tools: context.tools,\n },\n options\n );\n return minted.value;\n}\n\nfunction initiation(context: RealtimeContext, firstMessage: string | undefined): JsonObject {\n const agent: JsonObject = { prompt: { prompt: context.instructions } };\n if (firstMessage !== undefined) agent.first_message = firstMessage;\n return {\n type: 'conversation_initiation_client_data',\n conversation_config_override: { agent, tts: { voice_id: context.voice } },\n dynamic_variables: {},\n custom_llm_extra_body: {},\n };\n}\n\nclass ElevenLabsSession implements RealtimeSession {\n readonly model: RealtimeModel<string>;\n readonly #context: RealtimeContext;\n readonly #wire: VoiceWire;\n readonly #now: () => number;\n readonly #started: number;\n readonly #ledger = new RealtimeLedger();\n readonly #events = pushQueue<RealtimeEvent>();\n readonly #turn: TalkTurn = { vad: false, dropAudio: false, lastInterruptId: -1 };\n #closed = false;\n #usage: Usage | undefined;\n\n constructor(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n wire: VoiceWire,\n now: () => number\n ) {\n this.model = model;\n this.#context = context;\n this.#wire = wire;\n this.#now = now;\n this.#started = now();\n }\n\n start(firstMessage: string | undefined): void {\n this.#send(initiation(this.#context, firstMessage));\n void this.#read();\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new VoiceRequestError('error', 'session is closed');\n }\n\n #send(event: JsonObject): void {\n this.#wire.send(JSON.stringify(event));\n }\n\n #emit(event: RealtimeEvent): void {\n if (this.#closed) return;\n this.#ledger.observe(event);\n this.#events.push(event);\n }\n\n appendAudio(chunk: AudioChunk): void {\n this.#assertOpen();\n this.#ledger.addInput(chunk);\n this.#send({ user_audio_chunk: encodeAudioBase64(chunk.data) });\n }\n\n sendText(value: string): void {\n this.#assertOpen();\n this.#ledger.addText();\n this.#send({ type: 'user_message', text: value });\n }\n\n inject(value: string, delivery: Delivery): void {\n this.#assertOpen();\n this.#ledger.submit(this.model.delivery, delivery, () => {\n this.#assertOpen();\n this.#ledger.addText();\n this.#send(injectMessage(value, delivery));\n });\n }\n\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void {\n this.#assertOpen();\n const send = () => {\n this.#send(toolResult(callId, result));\n };\n if (delivery === undefined) {\n send();\n return;\n }\n this.#ledger.submit(this.model.delivery, delivery, () => {\n this.#assertOpen();\n send();\n });\n }\n\n interrupt(heard?: { itemId: string; heardMs: number }): void {\n this.#assertOpen();\n planTruncation(this.model.truncation, heard);\n this.#turn.dropAudio = true;\n this.#ledger.settle();\n }\n\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void {\n this.#assertOpen();\n if (patch.tools !== undefined && this.model.liveToolUpdate === false) {\n throw new VoiceRequestError('error', 'live tool update is unsupported');\n }\n if (patch.tools !== undefined) this.#context.tools = patch.tools;\n if (patch.instructions !== undefined) {\n this.#context.instructions = patch.instructions;\n this.#send({\n type: 'contextual_update',\n text: patch.instructions,\n context_id: 'pi-audio',\n });\n }\n if (patch.voice !== undefined) this.#context.voice = patch.voice;\n }\n\n async close(): Promise<{ usage: Usage }> {\n if (this.#usage !== undefined) return { usage: this.#usage };\n this.#ledger.settle();\n this.#closed = true;\n this.#wire.close();\n this.#events.end();\n const inputSeconds = Math.max(0, this.#now() - this.#started) / 1000;\n this.#usage = priceAudio(this.model.audioCost, {\n inputSeconds,\n outputSeconds: this.#ledger.outputSeconds,\n textMessages: this.#ledger.textMessages,\n });\n return { usage: this.#usage };\n }\n\n [Symbol.asyncIterator](): AsyncIterator<RealtimeEvent> {\n return this.#events.iterable[Symbol.asyncIterator]();\n }\n\n #onRaw(raw: string): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n this.#emit({ type: 'error', error: 'invalid realtime event', fatal: false });\n return;\n }\n if (!isRecord(parsed)) return;\n const ingested = ingestElevenLabs(this.#turn, parsed, this.model.outputFormat);\n for (const reply of ingested.replies) this.#send(reply);\n for (const event of ingested.events) this.#emit(event);\n }\n\n #fail(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error);\n this.#emit({ type: 'error', error: message, fatal: true });\n }\n\n async #read(): Promise<void> {\n try {\n for await (const raw of this.#wire.incoming) this.#onRaw(raw);\n } catch (error) {\n this.#fail(error);\n } finally {\n this.#closed = true;\n this.#events.end();\n }\n }\n}\n\nexport async function connectElevenLabs(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: ElevenLabsOptions = {}\n): Promise<RealtimeSession> {\n if (options?.signal?.aborted === true) throw new VoiceRequestError('aborted', 'session aborted');\n const agent = await ensureAgent(model, context, options, settings);\n const url = await socketUrl(model, agent.agentId, context, options);\n const routed = realtimeSocket(url, options, []);\n const target: VoiceWireTarget = {\n url: routed.url,\n protocols: routed.protocols,\n headers: socketHeaders(options),\n };\n const open =\n settings.wire !== undefined\n ? settings.wire\n : (wireTarget: VoiceWireTarget) => openVoiceSocket(wireTarget, settings.socket);\n const now = settings.now !== undefined ? settings.now : Date.now;\n const session = new ElevenLabsSession(model, context, open(target), now);\n session.start(settings.firstMessage);\n return session;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
function memoryHostedCache() {
|
|
3
|
+
const agents = /* @__PURE__ */ new Map();
|
|
4
|
+
return {
|
|
5
|
+
get(key) {
|
|
6
|
+
return agents.get(key);
|
|
7
|
+
},
|
|
8
|
+
set(key, agent) {
|
|
9
|
+
agents.set(key, agent);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function hostedCacheKey(provider, name) {
|
|
14
|
+
return `${provider}\0${name}`;
|
|
15
|
+
}
|
|
16
|
+
function stable(value) {
|
|
17
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
18
|
+
if (value === null || typeof value !== "object") return value;
|
|
19
|
+
const source = value;
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const key of Object.keys(source).sort()) {
|
|
22
|
+
const next = source[key];
|
|
23
|
+
if (next !== void 0) out[key] = stable(next);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function hashHostedSpec(spec) {
|
|
28
|
+
return createHash("sha256").update(JSON.stringify(stable(spec))).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
hashHostedSpec,
|
|
32
|
+
hostedCacheKey,
|
|
33
|
+
memoryHostedCache
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=hosted-agent.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/hosted-agent.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto';\nimport type { Model, Tool } from '@earendil-works/pi-ai';\n\nexport type CustomLlm = Pick<\n Model<'openai-completions' | 'openai-responses'>,\n 'id' | 'api' | 'baseUrl' | 'headers'\n>;\n\nexport interface HostedAgentSpec {\n name: string;\n instructions: string;\n tools: Tool[];\n voice: string;\n llm: string | CustomLlm;\n firstMessage?: string;\n clientToolTimeoutMs?: number;\n}\n\nexport interface HostedAgent {\n agentId: string;\n specHash: string;\n publishedAt: number;\n}\n\nexport interface HostedCache {\n get(key: string): HostedAgent | undefined;\n set(key: string, agent: HostedAgent): void;\n}\n\nexport function memoryHostedCache(): HostedCache {\n const agents = new Map<string, HostedAgent>();\n return {\n get(key) {\n return agents.get(key);\n },\n set(key, agent) {\n agents.set(key, agent);\n },\n };\n}\n\nexport function hostedCacheKey(provider: string, name: string): string {\n return `${provider}\\0${name}`;\n}\n\nfunction stable(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(stable);\n if (value === null || typeof value !== 'object') return value;\n const source = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(source).sort()) {\n const next = source[key];\n if (next !== undefined) out[key] = stable(next);\n }\n return out;\n}\n\nexport function hashHostedSpec(spec: HostedAgentSpec): string {\n return createHash('sha256')\n .update(JSON.stringify(stable(spec)))\n .digest('hex');\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AA6BpB,SAAS,oBAAiC;AAC/C,QAAM,SAAS,oBAAI,IAAyB;AAC5C,SAAO;AAAA,IACL,IAAI,KAAK;AACP,aAAO,OAAO,IAAI,GAAG;AAAA,IACvB;AAAA,IACA,IAAI,KAAK,OAAO;AACd,aAAO,IAAI,KAAK,KAAK;AAAA,IACvB;AAAA,EACF;AACF;AAEO,SAAS,eAAe,UAAkB,MAAsB;AACrE,SAAO,GAAG,QAAQ,KAAK,IAAI;AAC7B;AAEA,SAAS,OAAO,OAAyB;AACvC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,MAAM;AACjD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAM,SAAS;AACf,QAAM,MAA+B,CAAC;AACtC,aAAW,OAAO,OAAO,KAAK,MAAM,EAAE,KAAK,GAAG;AAC5C,UAAM,OAAO,OAAO,GAAG;AACvB,QAAI,SAAS,OAAW,KAAI,GAAG,IAAI,OAAO,IAAI;AAAA,EAChD;AACA,SAAO;AACT;AAEO,SAAS,eAAe,MAA+B;AAC5D,SAAO,WAAW,QAAQ,EACvB,OAAO,KAAK,UAAU,OAAO,IAAI,CAAC,CAAC,EACnC,OAAO,KAAK;AACjB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
speak,
|
|
24
24
|
transcribe
|
|
25
25
|
} from "./registry.js";
|
|
26
|
+
import { followBed } from "./soundscape.js";
|
|
26
27
|
import { SpeechEventStream } from "./speech.js";
|
|
27
28
|
import { askAgent, bindThinker, talkerTools } from "./talker.js";
|
|
28
29
|
import { mintRealtimeToken } from "./tokens.js";
|
|
@@ -46,6 +47,7 @@ export {
|
|
|
46
47
|
decodeAudioBase64,
|
|
47
48
|
encodeAudioBase64,
|
|
48
49
|
energyVad,
|
|
50
|
+
followBed,
|
|
49
51
|
getVoiceModel,
|
|
50
52
|
listVoiceModels,
|
|
51
53
|
listen,
|
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 type { CaptionHistoryEntry } from './captions.ts';\nexport { captionsFromHistory } from './captions.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 { RealtimeConstraints } from './tokens.ts';\nexport { mintRealtimeToken } from './tokens.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;AAE3B,SAAS,2BAA2B;AACpC;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;AAEnD,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AAazC,SAAS,kBAAkB;AAS3B,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
|
|
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 type { CaptionHistoryEntry } from './captions.ts';\nexport { captionsFromHistory } from './captions.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 { AudioOut, BedChannel, Cue, CueSource, Soundscape } from './soundscape.ts';\nexport { followBed } from './soundscape.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 { RealtimeConstraints } from './tokens.ts';\nexport { mintRealtimeToken } from './tokens.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;AAE3B,SAAS,2BAA2B;AACpC;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,iBAAiB;AAE1B,SAAS,yBAAyB;AAUlC,SAAS,UAAU,aAAa,mBAAmB;AAEnD,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AAazC,SAAS,kBAAkB;AAS3B,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
function isAudioOut(out) {
|
|
2
|
+
return "speech" in out && "bed" in out;
|
|
3
|
+
}
|
|
4
|
+
function followBed(bed, soundscape, wait) {
|
|
5
|
+
const schedule = wait === void 0 ? (run, delayMs) => setTimeout(run, delayMs) : wait;
|
|
6
|
+
let previous = { phase: "idle" };
|
|
7
|
+
let timer;
|
|
8
|
+
let generation = 0;
|
|
9
|
+
let playing;
|
|
10
|
+
const clearTimer = () => {
|
|
11
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
12
|
+
timer = void 0;
|
|
13
|
+
};
|
|
14
|
+
const stop = () => {
|
|
15
|
+
generation += 1;
|
|
16
|
+
clearTimer();
|
|
17
|
+
};
|
|
18
|
+
const observe = (phase) => {
|
|
19
|
+
const cues = listCues(soundscape.cue(phase, previous));
|
|
20
|
+
previous = phase;
|
|
21
|
+
const fade = fadeOf(soundscape);
|
|
22
|
+
bed.duck(duckFor(phase, soundscape), fade);
|
|
23
|
+
for (const cue of cues) {
|
|
24
|
+
if (cue.kind === "oneShot") bed.play(cue, fade);
|
|
25
|
+
}
|
|
26
|
+
if (cues.some((cue) => cue.kind === "silence")) {
|
|
27
|
+
generation += 1;
|
|
28
|
+
clearTimer();
|
|
29
|
+
playing = void 0;
|
|
30
|
+
bed.stop(fade);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const loop = firstLoop(cues);
|
|
34
|
+
if (!loop) {
|
|
35
|
+
generation += 1;
|
|
36
|
+
clearTimer();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const key = sourceKey(loop.source);
|
|
40
|
+
if (playing === key) {
|
|
41
|
+
generation += 1;
|
|
42
|
+
clearTimer();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
generation += 1;
|
|
46
|
+
const token = generation;
|
|
47
|
+
clearTimer();
|
|
48
|
+
const start = () => {
|
|
49
|
+
if (token !== generation) return;
|
|
50
|
+
playing = key;
|
|
51
|
+
bed.play(loop, fade);
|
|
52
|
+
};
|
|
53
|
+
const grace = graceOf(soundscape);
|
|
54
|
+
if (grace <= 0) {
|
|
55
|
+
start();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
timer = schedule(start, grace);
|
|
59
|
+
};
|
|
60
|
+
return { observe, stop };
|
|
61
|
+
}
|
|
62
|
+
function listCues(cue) {
|
|
63
|
+
if ("kind" in cue) return [cue];
|
|
64
|
+
return cue;
|
|
65
|
+
}
|
|
66
|
+
function firstLoop(cues) {
|
|
67
|
+
for (const cue of cues) {
|
|
68
|
+
if (cue.kind === "loop") return cue;
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
function sourceKey(source) {
|
|
73
|
+
if ("url" in source) return `url:${source.url}`;
|
|
74
|
+
if ("buffer" in source) return `buffer:${source.buffer.length}`;
|
|
75
|
+
return "synth";
|
|
76
|
+
}
|
|
77
|
+
function fadeOf(soundscape) {
|
|
78
|
+
const fade = soundscape.fadeMs;
|
|
79
|
+
if (fade === void 0) return 0;
|
|
80
|
+
return fade;
|
|
81
|
+
}
|
|
82
|
+
function graceOf(soundscape) {
|
|
83
|
+
const grace = soundscape.graceMs;
|
|
84
|
+
if (grace === void 0) return 0;
|
|
85
|
+
return grace;
|
|
86
|
+
}
|
|
87
|
+
function duckFor(phase, soundscape) {
|
|
88
|
+
if (phase.phase === "speaking") {
|
|
89
|
+
const gain = soundscape.duckUnderSpeech;
|
|
90
|
+
if (gain === void 0) return 1;
|
|
91
|
+
return gain;
|
|
92
|
+
}
|
|
93
|
+
if (phase.phase === "listening") {
|
|
94
|
+
const gain = soundscape.duckUnderUser;
|
|
95
|
+
if (gain === void 0) return 1;
|
|
96
|
+
return gain;
|
|
97
|
+
}
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
followBed,
|
|
102
|
+
isAudioOut
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=soundscape.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/soundscape.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioSink, AudioSource } from './audio.ts';\nimport type { VoicePhase } from './turns.ts';\n\nexport type CueSource =\n | { url: string }\n | { buffer: AudioBuffer }\n | { synth: (ctx: BaseAudioContext) => AudioNode };\n\nexport type Cue =\n | { kind: 'loop'; source: CueSource; gain: number }\n | { kind: 'oneShot'; source: CueSource; gain: number }\n | { kind: 'keep' }\n | { kind: 'silence' };\n\nexport interface Soundscape {\n cue(next: VoicePhase, previous: VoicePhase): Cue | readonly Cue[];\n graceMs?: number;\n fadeMs?: number;\n duckUnderSpeech?: number;\n duckUnderUser?: number;\n}\n\nexport interface BedChannel {\n play(cue: Cue, fadeMs: number): void;\n duck(gain: number, rampMs: number): void;\n stop(fadeMs: number): void;\n}\n\nexport interface AudioOut {\n readonly context: BaseAudioContext;\n speech: AudioSink;\n bed: BedChannel;\n readonly reference: AudioSource;\n readonly playback: 'webaudio' | 'rtc-loopback';\n}\n\nexport interface BedFollower {\n observe(phase: VoicePhase): void;\n stop(): void;\n}\n\nexport function isAudioOut(out: AudioSink | AudioOut): out is AudioOut {\n return 'speech' in out && 'bed' in out;\n}\n\nexport function followBed(\n bed: BedChannel,\n soundscape: Soundscape,\n wait?: (run: () => void, delayMs: number) => ReturnType<typeof setTimeout>\n): BedFollower {\n const schedule =\n wait === undefined ? (run: () => void, delayMs: number) => setTimeout(run, delayMs) : wait;\n let previous: VoicePhase = { phase: 'idle' };\n let timer: ReturnType<typeof setTimeout> | undefined;\n let generation = 0;\n let playing: string | undefined;\n\n const clearTimer = () => {\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n };\n\n const stop = () => {\n generation += 1;\n clearTimer();\n };\n\n const observe = (phase: VoicePhase) => {\n const cues = listCues(soundscape.cue(phase, previous));\n previous = phase;\n const fade = fadeOf(soundscape);\n bed.duck(duckFor(phase, soundscape), fade);\n for (const cue of cues) {\n if (cue.kind === 'oneShot') bed.play(cue, fade);\n }\n if (cues.some((cue) => cue.kind === 'silence')) {\n generation += 1;\n clearTimer();\n playing = undefined;\n bed.stop(fade);\n return;\n }\n const loop = firstLoop(cues);\n if (!loop) {\n generation += 1;\n clearTimer();\n return;\n }\n const key = sourceKey(loop.source);\n if (playing === key) {\n generation += 1;\n clearTimer();\n return;\n }\n generation += 1;\n const token = generation;\n clearTimer();\n const start = () => {\n if (token !== generation) return;\n playing = key;\n bed.play(loop, fade);\n };\n const grace = graceOf(soundscape);\n if (grace <= 0) {\n start();\n return;\n }\n timer = schedule(start, grace);\n };\n\n return { observe, stop };\n}\n\nfunction listCues(cue: Cue | readonly Cue[]): readonly Cue[] {\n if ('kind' in cue) return [cue];\n return cue;\n}\n\nfunction firstLoop(cues: readonly Cue[]): Extract<Cue, { kind: 'loop' }> | undefined {\n for (const cue of cues) {\n if (cue.kind === 'loop') return cue;\n }\n return undefined;\n}\n\nfunction sourceKey(source: CueSource): string {\n if ('url' in source) return `url:${source.url}`;\n if ('buffer' in source) return `buffer:${source.buffer.length}`;\n return 'synth';\n}\n\nfunction fadeOf(soundscape: Soundscape): number {\n const fade = soundscape.fadeMs;\n if (fade === undefined) return 0;\n return fade;\n}\n\nfunction graceOf(soundscape: Soundscape): number {\n const grace = soundscape.graceMs;\n if (grace === undefined) return 0;\n return grace;\n}\n\nfunction duckFor(phase: VoicePhase, soundscape: Soundscape): number {\n if (phase.phase === 'speaking') {\n const gain = soundscape.duckUnderSpeech;\n if (gain === undefined) return 1;\n return gain;\n }\n if (phase.phase === 'listening') {\n const gain = soundscape.duckUnderUser;\n if (gain === undefined) return 1;\n return gain;\n }\n return 1;\n}\n"],
|
|
5
|
+
"mappings": "AAyCO,SAAS,WAAW,KAA4C;AACrE,SAAO,YAAY,OAAO,SAAS;AACrC;AAEO,SAAS,UACd,KACA,YACA,MACa;AACb,QAAM,WACJ,SAAS,SAAY,CAAC,KAAiB,YAAoB,WAAW,KAAK,OAAO,IAAI;AACxF,MAAI,WAAuB,EAAE,OAAO,OAAO;AAC3C,MAAI;AACJ,MAAI,aAAa;AACjB,MAAI;AAEJ,QAAM,aAAa,MAAM;AACvB,QAAI,UAAU,OAAW,cAAa,KAAK;AAC3C,YAAQ;AAAA,EACV;AAEA,QAAM,OAAO,MAAM;AACjB,kBAAc;AACd,eAAW;AAAA,EACb;AAEA,QAAM,UAAU,CAAC,UAAsB;AACrC,UAAM,OAAO,SAAS,WAAW,IAAI,OAAO,QAAQ,CAAC;AACrD,eAAW;AACX,UAAM,OAAO,OAAO,UAAU;AAC9B,QAAI,KAAK,QAAQ,OAAO,UAAU,GAAG,IAAI;AACzC,eAAW,OAAO,MAAM;AACtB,UAAI,IAAI,SAAS,UAAW,KAAI,KAAK,KAAK,IAAI;AAAA,IAChD;AACA,QAAI,KAAK,KAAK,CAAC,QAAQ,IAAI,SAAS,SAAS,GAAG;AAC9C,oBAAc;AACd,iBAAW;AACX,gBAAU;AACV,UAAI,KAAK,IAAI;AACb;AAAA,IACF;AACA,UAAM,OAAO,UAAU,IAAI;AAC3B,QAAI,CAAC,MAAM;AACT,oBAAc;AACd,iBAAW;AACX;AAAA,IACF;AACA,UAAM,MAAM,UAAU,KAAK,MAAM;AACjC,QAAI,YAAY,KAAK;AACnB,oBAAc;AACd,iBAAW;AACX;AAAA,IACF;AACA,kBAAc;AACd,UAAM,QAAQ;AACd,eAAW;AACX,UAAM,QAAQ,MAAM;AAClB,UAAI,UAAU,WAAY;AAC1B,gBAAU;AACV,UAAI,KAAK,MAAM,IAAI;AAAA,IACrB;AACA,UAAM,QAAQ,QAAQ,UAAU;AAChC,QAAI,SAAS,GAAG;AACd,YAAM;AACN;AAAA,IACF;AACA,YAAQ,SAAS,OAAO,KAAK;AAAA,EAC/B;AAEA,SAAO,EAAE,SAAS,KAAK;AACzB;AAEA,SAAS,SAAS,KAA2C;AAC3D,MAAI,UAAU,IAAK,QAAO,CAAC,GAAG;AAC9B,SAAO;AACT;AAEA,SAAS,UAAU,MAAkE;AACnF,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,SAAS,OAAQ,QAAO;AAAA,EAClC;AACA,SAAO;AACT;AAEA,SAAS,UAAU,QAA2B;AAC5C,MAAI,SAAS,OAAQ,QAAO,OAAO,OAAO,GAAG;AAC7C,MAAI,YAAY,OAAQ,QAAO,UAAU,OAAO,OAAO,MAAM;AAC7D,SAAO;AACT;AAEA,SAAS,OAAO,YAAgC;AAC9C,QAAM,OAAO,WAAW;AACxB,MAAI,SAAS,OAAW,QAAO;AAC/B,SAAO;AACT;AAEA,SAAS,QAAQ,YAAgC;AAC/C,QAAM,QAAQ,WAAW;AACzB,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO;AACT;AAEA,SAAS,QAAQ,OAAmB,YAAgC;AAClE,MAAI,MAAM,UAAU,YAAY;AAC9B,UAAM,OAAO,WAAW;AACxB,QAAI,SAAS,OAAW,QAAO;AAC/B,WAAO;AAAA,EACT;AACA,MAAI,MAAM,UAAU,aAAa;AAC/B,UAAM,OAAO,WAAW;AACxB,QAAI,SAAS,OAAW,QAAO;AAC/B,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/turns.js
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
import { spokenOffset, utteranceIndex } from "./captions.js";
|
|
5
5
|
import { VoiceRequestError } from "./errors.js";
|
|
6
6
|
import { pushQueue } from "./queue.js";
|
|
7
|
+
import {
|
|
8
|
+
followBed,
|
|
9
|
+
isAudioOut
|
|
10
|
+
} from "./soundscape.js";
|
|
7
11
|
import {
|
|
8
12
|
createSpokenState,
|
|
9
13
|
spokenPieces
|
|
@@ -15,12 +19,13 @@ function voiceTurns(models, conversation, mic, out, options, signal) {
|
|
|
15
19
|
models,
|
|
16
20
|
conversation,
|
|
17
21
|
mic,
|
|
18
|
-
out,
|
|
22
|
+
out: speechOf(out),
|
|
19
23
|
options,
|
|
20
24
|
signal,
|
|
21
25
|
phases,
|
|
22
26
|
captions,
|
|
23
|
-
maps: []
|
|
27
|
+
maps: [],
|
|
28
|
+
bed: attachBed(out, options.soundscape)
|
|
24
29
|
};
|
|
25
30
|
const done = runSession(session).finally(() => {
|
|
26
31
|
phases.end();
|
|
@@ -45,7 +50,7 @@ async function runSession(session) {
|
|
|
45
50
|
if (!active) return;
|
|
46
51
|
notePlayback(session, active, tag, offsetMs);
|
|
47
52
|
});
|
|
48
|
-
const setPhase = phaseSetter(session.phases);
|
|
53
|
+
const setPhase = phaseSetter(session.phases, session.bed);
|
|
49
54
|
try {
|
|
50
55
|
if (session.signal.aborted) return;
|
|
51
56
|
setPhase({ phase: "listening" });
|
|
@@ -74,6 +79,7 @@ async function runSession(session) {
|
|
|
74
79
|
else if (active) await active.done;
|
|
75
80
|
stopPlayback();
|
|
76
81
|
setPhase({ phase: "idle" });
|
|
82
|
+
session.bed.stop();
|
|
77
83
|
}
|
|
78
84
|
}
|
|
79
85
|
function turnMeta(options, confidence) {
|
|
@@ -317,13 +323,31 @@ function shouldBarge(mode, event) {
|
|
|
317
323
|
if (mode === "interrupt") return event.type === "speech_start";
|
|
318
324
|
return event.type === "partial" && event.text.trim().length > 0;
|
|
319
325
|
}
|
|
320
|
-
function
|
|
326
|
+
function speechOf(out) {
|
|
327
|
+
if (isAudioOut(out)) return out.speech;
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function attachBed(out, soundscape) {
|
|
331
|
+
if (!soundscape) return idleBed();
|
|
332
|
+
if (!isAudioOut(out)) return idleBed();
|
|
333
|
+
return followBed(out.bed, soundscape);
|
|
334
|
+
}
|
|
335
|
+
function idleBed() {
|
|
336
|
+
return {
|
|
337
|
+
observe() {
|
|
338
|
+
},
|
|
339
|
+
stop() {
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function phaseSetter(phases, bed) {
|
|
321
344
|
let previous = "";
|
|
322
345
|
return (phase) => {
|
|
323
346
|
const key = JSON.stringify(phase);
|
|
324
347
|
if (key === previous) return;
|
|
325
348
|
previous = key;
|
|
326
349
|
phases.push(phase);
|
|
350
|
+
bed.observe(phase);
|
|
327
351
|
};
|
|
328
352
|
}
|
|
329
353
|
function linkSignal(parent) {
|