@ai-ecoverse/pi-audio 1.20.0 → 1.21.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 +3 -1
- package/dist/captions.js +16 -8
- package/dist/captions.js.map +2 -2
- package/dist/durable/index.js +3 -1
- package/dist/durable/index.js.map +2 -2
- package/dist/fish-agents/events.js +94 -0
- package/dist/fish-agents/events.js.map +7 -0
- package/dist/fish-agents/index.js +57 -0
- package/dist/fish-agents/index.js.map +7 -0
- package/dist/fish-agents/provision.js +178 -0
- package/dist/fish-agents/provision.js.map +7 -0
- package/dist/fish-agents/room.js +80 -0
- package/dist/fish-agents/room.js.map +7 -0
- package/dist/fish-agents/session.js +304 -0
- package/dist/fish-agents/session.js.map +7 -0
- package/dist/realtime.js.map +2 -2
- package/dist/soundscape.js +14 -2
- package/dist/soundscape.js.map +2 -2
- package/dist/tokens.js +11 -1
- package/dist/tokens.js.map +2 -2
- package/dist/turns.js +78 -30
- package/dist/turns.js.map +2 -2
- package/dist/types/fish-agents/events.d.ts +21 -0
- package/dist/types/fish-agents/index.d.ts +11 -0
- package/dist/types/fish-agents/provision.d.ts +11 -0
- package/dist/types/fish-agents/room.d.ts +46 -0
- package/dist/types/fish-agents/session.d.ts +19 -0
- package/dist/types/realtime.d.ts +1 -0
- package/dist/vad-listen.js +43 -12
- package/dist/vad-listen.js.map +2 -2
- package/dist/vad.js +15 -7
- package/dist/vad.js.map +2 -2
- package/package.json +5 -1
package/dist/tokens.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tokens.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ProviderRequestOptions, Tool } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from './errors.ts';\nimport type { RealtimeModel } from './models.ts';\nimport type { RealtimeToken } from './realtime.ts';\n\nexport interface RealtimeConstraints {\n instructions?: string;\n tools?: Tool[];\n voice?: string;\n uses?: number;\n agentId?: string;\n}\n\nconst openaiSecrets = 'https://api.openai.com/v1/realtime/client_secrets';\nconst xaiSecrets = 'https://api.x.ai/v1/realtime/client_secrets';\nconst geminiTokens = 'https://generativelanguage.googleapis.com/v1beta/auth_tokens';\nconst elevenSigned = 'https://api.elevenlabs.io/v1/convai/conversation/get-signed-url';\nconst elevenWebRtc = 'https://api.elevenlabs.io/v1/convai/conversation/token';\nconst fishSessions = 'https://api.fish.audio/v1/agent/sessions';\nconst fifteenMinutes = 15 * 60 * 1000;\nconst thirtyMinutes = 30 * 60 * 1000;\nconst oneMinute = 60 * 1000;\nconst xaiLifetimeSeconds = 300;\n\ntype Json = Record<string, unknown>;\n\ninterface MintCall {\n url: string;\n method: 'GET' | 'POST';\n headers: Record<string, string>;\n body?: string;\n field: string;\n expires: (json: Json, fallback: number) => number;\n fallbackExpires: number;\n}\n\nfunction apiKey(options: ProviderRequestOptions): string {\n const key = options.apiKey;\n if (typeof key === 'string' && key.length > 0) return key;\n throw new VoiceRequestError('error', 'missing realtime credential');\n}\n\nfunction mergedHeaders(\n options: ProviderRequestOptions,\n defaults: Record<string, string>\n): Record<string, string> {\n const headers = { ...defaults };\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 omitUndefined(source: Json): Json {\n const out: Json = {};\n for (const [key, value] of Object.entries(source)) {\n if (value !== undefined) out[key] = value;\n }\n return out;\n}\n\nfunction functionTool(tool: Tool): Json {\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction declaration(tool: Tool): Json {\n return {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction requiredAgent(constraints: RealtimeConstraints): string {\n const id = constraints.agentId;\n if (typeof id === 'string' && id.length > 0) return id;\n throw new VoiceRequestError('error', 'missing agent id');\n}\n\nfunction expiresAt(value: unknown, fallback: number): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value < 1e12 ? value * 1000 : value;\n }\n if (typeof value === 'string') {\n const parsed = Date.parse(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return fallback;\n}\n\nfunction fromField(field: string): (json: Json, fallback: number) => number {\n return (json, fallback) => expiresAt(json[field], fallback);\n}\n\nfunction openaiBody(model: RealtimeModel<string>, constraints: RealtimeConstraints): Json {\n const session = omitUndefined({\n type: 'realtime',\n model: model.id,\n instructions: constraints.instructions,\n tools: constraints.tools?.map(functionTool),\n });\n if (constraints.voice !== undefined) session.audio = { output: { voice: constraints.voice } };\n return { session };\n}\n\nfunction geminiBody(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n now: number\n): Json {\n const instructions = constraints.instructions;\n const voice = constraints.voice;\n const tools = constraints.tools;\n const config = omitUndefined({\n responseModalities: ['AUDIO'],\n sessionResumption: {},\n systemInstruction: instructions === undefined ? undefined : { parts: [{ text: instructions }] },\n speechConfig:\n voice === undefined\n ? undefined\n : { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } },\n tools: tools === undefined ? undefined : [{ functionDeclarations: tools.map(declaration) }],\n });\n return {\n uses: constraints.uses ?? 1,\n expireTime: new Date(now + thirtyMinutes).toISOString(),\n newSessionExpireTime: new Date(now + oneMinute).toISOString(),\n liveConnectConstraints: { model: `models/${model.id}`, config },\n };\n}\n\nfunction fishBody(constraints: RealtimeConstraints): Json {\n const overrides = omitUndefined({\n system_prompt: constraints.instructions,\n voice_id: constraints.voice,\n });\n return omitUndefined({\n agent_id: requiredAgent(constraints),\n overrides: Object.keys(overrides).length > 0 ? overrides : undefined,\n });\n}\n\nfunction webrtcOnly(model: RealtimeModel<string>): boolean {\n return model.transports.length === 1 && model.transports[0] === 'webrtc';\n}\n\nfunction elevenUrl(model: RealtimeModel<string>, constraints: RealtimeConstraints): string {\n const url = new URL(webrtcOnly(model) ? elevenWebRtc : elevenSigned);\n url.searchParams.set('agent_id', requiredAgent(constraints));\n return url.toString();\n}\n\nfunction mintCall(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options: ProviderRequestOptions,\n key: string,\n now: number\n): MintCall {\n const jsonHeaders = { 'Content-Type': 'application/json' };\n switch (model.api) {\n case 'openai-realtime':\n return {\n url: openaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(openaiBody(model, constraints)),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n case 'xai-realtime':\n return {\n url: xaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify({ expires_after: { seconds: xaiLifetimeSeconds } }),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now + xaiLifetimeSeconds * 1000,\n };\n case 'gemini-live':\n return {\n url: geminiTokens,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, 'x-goog-api-key': key }),\n body: JSON.stringify(geminiBody(model, constraints, now)),\n field: 'name',\n expires: fromField('expireTime'),\n fallbackExpires: now + thirtyMinutes,\n };\n case 'elevenlabs-agents':\n return {\n url: elevenUrl(model, constraints),\n method: 'GET',\n headers: mergedHeaders(options, { 'xi-api-key': key }),\n field: webrtcOnly(model) ? 'token' : 'signed_url',\n expires: fromField('expires_at'),\n fallbackExpires: now + fifteenMinutes,\n };\n case 'fish-agents':\n return {\n url: fishSessions,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(fishBody(constraints)),\n field: 'token',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n default:\n throw new VoiceRequestError('error', 'unsupported realtime api');\n }\n}\n\nfunction readValue(json: unknown, field: string): string {\n if (typeof json !== 'object' || json === null) {\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n }\n const value = (json as Json)[field];\n if (typeof value === 'string' && value.length > 0) return value;\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n}\n\nexport async function mintRealtimeToken(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options?: ProviderRequestOptions\n): Promise<RealtimeToken> {\n const settings = options ?? {};\n const key = apiKey(settings);\n const now = Date.now();\n const call = mintCall(model, constraints, settings, key, now);\n const fetch = settings.fetch ?? globalThis.fetch;\n const response = await fetch(call.url, {\n method: call.method,\n headers: call.headers,\n body: call.body,\n signal: settings.signal,\n });\n if (!response.ok) throw new VoiceRequestError('error', `mint failed (${response.status})`);\n const json: unknown = await response.json();\n
|
|
5
|
-
"mappings": "AACA,SAAS,yBAAyB;AAYlC,MAAM,gBAAgB;AACtB,MAAM,aAAa;AACnB,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,iBAAiB,KAAK,KAAK;AACjC,MAAM,gBAAgB,KAAK,KAAK;AAChC,MAAM,YAAY,KAAK;AACvB,MAAM,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions, Tool } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from './errors.ts';\nimport type { RealtimeModel } from './models.ts';\nimport type { RealtimeToken } from './realtime.ts';\n\nexport interface RealtimeConstraints {\n instructions?: string;\n tools?: Tool[];\n voice?: string;\n uses?: number;\n agentId?: string;\n}\n\nconst openaiSecrets = 'https://api.openai.com/v1/realtime/client_secrets';\nconst xaiSecrets = 'https://api.x.ai/v1/realtime/client_secrets';\nconst geminiTokens = 'https://generativelanguage.googleapis.com/v1beta/auth_tokens';\nconst elevenSigned = 'https://api.elevenlabs.io/v1/convai/conversation/get-signed-url';\nconst elevenWebRtc = 'https://api.elevenlabs.io/v1/convai/conversation/token';\nconst fishSessions = 'https://api.fish.audio/v1/agent/sessions';\nconst fifteenMinutes = 15 * 60 * 1000;\nconst thirtyMinutes = 30 * 60 * 1000;\nconst oneMinute = 60 * 1000;\nconst xaiLifetimeSeconds = 300;\n\ntype Json = Record<string, unknown>;\n\ninterface MintCall {\n url: string;\n method: 'GET' | 'POST';\n headers: Record<string, string>;\n body?: string;\n field: string;\n urlField?: string;\n expires: (json: Json, fallback: number) => number;\n fallbackExpires: number;\n}\n\nfunction apiKey(options: ProviderRequestOptions): string {\n const key = options.apiKey;\n if (typeof key === 'string' && key.length > 0) return key;\n throw new VoiceRequestError('error', 'missing realtime credential');\n}\n\nfunction mergedHeaders(\n options: ProviderRequestOptions,\n defaults: Record<string, string>\n): Record<string, string> {\n const headers = { ...defaults };\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 omitUndefined(source: Json): Json {\n const out: Json = {};\n for (const [key, value] of Object.entries(source)) {\n if (value !== undefined) out[key] = value;\n }\n return out;\n}\n\nfunction functionTool(tool: Tool): Json {\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction declaration(tool: Tool): Json {\n return {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters as unknown,\n };\n}\n\nfunction requiredAgent(constraints: RealtimeConstraints): string {\n const id = constraints.agentId;\n if (typeof id === 'string' && id.length > 0) return id;\n throw new VoiceRequestError('error', 'missing agent id');\n}\n\nfunction expiresAt(value: unknown, fallback: number): number {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return value < 1e12 ? value * 1000 : value;\n }\n if (typeof value === 'string') {\n const parsed = Date.parse(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n return fallback;\n}\n\nfunction fromField(field: string): (json: Json, fallback: number) => number {\n return (json, fallback) => expiresAt(json[field], fallback);\n}\n\nfunction openaiBody(model: RealtimeModel<string>, constraints: RealtimeConstraints): Json {\n const session = omitUndefined({\n type: 'realtime',\n model: model.id,\n instructions: constraints.instructions,\n tools: constraints.tools?.map(functionTool),\n });\n if (constraints.voice !== undefined) session.audio = { output: { voice: constraints.voice } };\n return { session };\n}\n\nfunction geminiBody(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n now: number\n): Json {\n const instructions = constraints.instructions;\n const voice = constraints.voice;\n const tools = constraints.tools;\n const config = omitUndefined({\n responseModalities: ['AUDIO'],\n sessionResumption: {},\n systemInstruction: instructions === undefined ? undefined : { parts: [{ text: instructions }] },\n speechConfig:\n voice === undefined\n ? undefined\n : { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } },\n tools: tools === undefined ? undefined : [{ functionDeclarations: tools.map(declaration) }],\n });\n return {\n uses: constraints.uses ?? 1,\n expireTime: new Date(now + thirtyMinutes).toISOString(),\n newSessionExpireTime: new Date(now + oneMinute).toISOString(),\n liveConnectConstraints: { model: `models/${model.id}`, config },\n };\n}\n\nfunction fishBody(constraints: RealtimeConstraints): Json {\n const overrides = omitUndefined({\n system_prompt: constraints.instructions,\n voice_id: constraints.voice,\n });\n return omitUndefined({\n agent_id: requiredAgent(constraints),\n overrides: Object.keys(overrides).length > 0 ? overrides : undefined,\n });\n}\n\nfunction webrtcOnly(model: RealtimeModel<string>): boolean {\n return model.transports.length === 1 && model.transports[0] === 'webrtc';\n}\n\nfunction elevenUrl(model: RealtimeModel<string>, constraints: RealtimeConstraints): string {\n const url = new URL(webrtcOnly(model) ? elevenWebRtc : elevenSigned);\n url.searchParams.set('agent_id', requiredAgent(constraints));\n return url.toString();\n}\n\nfunction mintCall(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options: ProviderRequestOptions,\n key: string,\n now: number\n): MintCall {\n const jsonHeaders = { 'Content-Type': 'application/json' };\n switch (model.api) {\n case 'openai-realtime':\n return {\n url: openaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(openaiBody(model, constraints)),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n case 'xai-realtime':\n return {\n url: xaiSecrets,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify({ expires_after: { seconds: xaiLifetimeSeconds } }),\n field: 'value',\n expires: fromField('expires_at'),\n fallbackExpires: now + xaiLifetimeSeconds * 1000,\n };\n case 'gemini-live':\n return {\n url: geminiTokens,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, 'x-goog-api-key': key }),\n body: JSON.stringify(geminiBody(model, constraints, now)),\n field: 'name',\n expires: fromField('expireTime'),\n fallbackExpires: now + thirtyMinutes,\n };\n case 'elevenlabs-agents':\n return {\n url: elevenUrl(model, constraints),\n method: 'GET',\n headers: mergedHeaders(options, { 'xi-api-key': key }),\n field: webrtcOnly(model) ? 'token' : 'signed_url',\n expires: fromField('expires_at'),\n fallbackExpires: now + fifteenMinutes,\n };\n case 'fish-agents':\n return {\n url: fishSessions,\n method: 'POST',\n headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),\n body: JSON.stringify(fishBody(constraints)),\n field: 'token',\n urlField: 'livekit_url',\n expires: fromField('expires_at'),\n fallbackExpires: now,\n };\n default:\n throw new VoiceRequestError('error', 'unsupported realtime api');\n }\n}\n\nfunction readValue(json: unknown, field: string): string {\n if (typeof json !== 'object' || json === null) {\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n }\n const value = (json as Json)[field];\n if (typeof value === 'string' && value.length > 0) return value;\n throw new VoiceRequestError('error', 'mint failed (empty token)');\n}\n\nfunction readUrl(json: unknown, field: string | undefined): string | undefined {\n if (field === undefined) return undefined;\n const value = (json as Json)[field];\n if (typeof value === 'string' && value.length > 0) return value;\n throw new VoiceRequestError('error', 'mint failed (missing livekit url)');\n}\n\nexport async function mintRealtimeToken(\n model: RealtimeModel<string>,\n constraints: RealtimeConstraints,\n options?: ProviderRequestOptions\n): Promise<RealtimeToken> {\n const settings = options ?? {};\n const key = apiKey(settings);\n const now = Date.now();\n const call = mintCall(model, constraints, settings, key, now);\n const fetch = settings.fetch ?? globalThis.fetch;\n const response = await fetch(call.url, {\n method: call.method,\n headers: call.headers,\n body: call.body,\n signal: settings.signal,\n });\n if (!response.ok) throw new VoiceRequestError('error', `mint failed (${response.status})`);\n const json: unknown = await response.json();\n const token: RealtimeToken = {\n value: readValue(json, call.field),\n expiresAt: call.expires(json as Json, call.fallbackExpires),\n model: model.id,\n };\n const url = readUrl(json, call.urlField);\n if (url !== undefined) token.url = url;\n return token;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/turns.js
CHANGED
|
@@ -26,6 +26,7 @@ function voiceTurns(models, conversation, mic, out, options, signal) {
|
|
|
26
26
|
phases,
|
|
27
27
|
captions,
|
|
28
28
|
maps: [],
|
|
29
|
+
clocks: /* @__PURE__ */ new WeakMap(),
|
|
29
30
|
bed: attachBed(out, options.soundscape)
|
|
30
31
|
};
|
|
31
32
|
const done = runSession(session).finally(() => {
|
|
@@ -52,12 +53,16 @@ async function runSession(session) {
|
|
|
52
53
|
notePlayback(session, active, tag, offsetMs);
|
|
53
54
|
});
|
|
54
55
|
const setPhase = phaseSetter(session.phases, session.bed);
|
|
56
|
+
const turnFailed = Promise.withResolvers();
|
|
55
57
|
try {
|
|
56
58
|
if (session.signal.aborted) return;
|
|
57
59
|
setPhase({ phase: "listening" });
|
|
58
|
-
|
|
60
|
+
const heard = session.models.listen(session.options.ears, session.mic, {
|
|
59
61
|
signal: session.signal
|
|
60
|
-
}))
|
|
62
|
+
})[Symbol.asyncIterator]();
|
|
63
|
+
while (true) {
|
|
64
|
+
const event = await nextVoiceEvent(heard, turnFailed.promise);
|
|
65
|
+
if (!event) break;
|
|
61
66
|
if (event.type === "error") {
|
|
62
67
|
if (event.reason === "aborted" || session.signal.aborted) return;
|
|
63
68
|
throw new VoiceRequestError(event.reason, event.error);
|
|
@@ -73,7 +78,7 @@ async function runSession(session) {
|
|
|
73
78
|
setPhase({ phase: "heard", text: event.text });
|
|
74
79
|
const meta = turnMeta(session.options, confidence);
|
|
75
80
|
confidence = void 0;
|
|
76
|
-
active = startTurn(session, setPhase, turnId, event, meta);
|
|
81
|
+
active = startTurn(session, setPhase, turnId, event, meta, turnFailed.reject);
|
|
77
82
|
}
|
|
78
83
|
} finally {
|
|
79
84
|
if (session.signal.aborted && active) await interrupt(session, active);
|
|
@@ -119,7 +124,12 @@ async function finishTurn(active) {
|
|
|
119
124
|
await active.done;
|
|
120
125
|
return void 0;
|
|
121
126
|
}
|
|
122
|
-
function
|
|
127
|
+
async function nextVoiceEvent(heard, failed) {
|
|
128
|
+
const step = await Promise.race([heard.next(), failed]);
|
|
129
|
+
if (step.done) return void 0;
|
|
130
|
+
return step.value;
|
|
131
|
+
}
|
|
132
|
+
function startTurn(session, setPhase, turnId, event, meta, rejectTurn) {
|
|
123
133
|
const linked = linkSignal(session.signal);
|
|
124
134
|
const active = {
|
|
125
135
|
controller: linked.controller,
|
|
@@ -127,11 +137,13 @@ function startTurn(session, setPhase, turnId, event, meta) {
|
|
|
127
137
|
turnId,
|
|
128
138
|
utteranceId: `${turnId}:agent`,
|
|
129
139
|
spokenUpTo: 0,
|
|
140
|
+
plays: [],
|
|
130
141
|
done: Promise.resolve()
|
|
131
142
|
};
|
|
132
143
|
active.done = performTurn(session, setPhase, active, event.text, event.at, meta).finally(() => {
|
|
133
144
|
linked.unlink();
|
|
134
145
|
});
|
|
146
|
+
void active.done.catch((error) => rejectTurn(error));
|
|
135
147
|
return active;
|
|
136
148
|
}
|
|
137
149
|
async function performTurn(session, setPhase, active, text, at, meta) {
|
|
@@ -180,7 +192,7 @@ async function readAgent(session, setPhase, active, message, meta, feed, maps) {
|
|
|
180
192
|
);
|
|
181
193
|
feed.push(piece.text);
|
|
182
194
|
}
|
|
183
|
-
if (state.entryId) bindEntry(maps, state.entryId);
|
|
195
|
+
if (state.entryId) bindEntry(session, active, maps, state.entryId);
|
|
184
196
|
}
|
|
185
197
|
} finally {
|
|
186
198
|
feed.end();
|
|
@@ -220,40 +232,75 @@ async function pipeSpeech(session, setPhase, active, audio, maps) {
|
|
|
220
232
|
if (event.type !== "audio") continue;
|
|
221
233
|
setPhase({ phase: "speaking" });
|
|
222
234
|
const span = translateSpan(event.span, maps);
|
|
223
|
-
const playing = {
|
|
224
|
-
start: event.span.start,
|
|
225
|
-
end: event.span.end,
|
|
226
|
-
durationMs: Math.round(chunkSeconds(event.chunk) * 1e3)
|
|
227
|
-
};
|
|
228
|
-
if (event.alignment) playing.alignment = event.alignment;
|
|
229
|
-
session.playing = playing;
|
|
230
235
|
const tag = { span };
|
|
231
236
|
if (event.alignment) tag.alignment = event.alignment;
|
|
237
|
+
session.clocks.set(tag, clockOf(event));
|
|
232
238
|
session.out.write(event.chunk, tag);
|
|
233
239
|
}
|
|
234
240
|
}
|
|
241
|
+
function clockOf(event) {
|
|
242
|
+
const clock = {
|
|
243
|
+
start: event.span.start,
|
|
244
|
+
end: event.span.end,
|
|
245
|
+
durationMs: chunkDurationMs(event)
|
|
246
|
+
};
|
|
247
|
+
if (event.alignment) clock.alignment = event.alignment;
|
|
248
|
+
return clock;
|
|
249
|
+
}
|
|
250
|
+
function chunkDurationMs(event) {
|
|
251
|
+
const measured = Math.round(chunkSeconds(event.chunk) * 1e3);
|
|
252
|
+
if (measured > 0) return measured;
|
|
253
|
+
const aligned = alignedDuration(event.alignment);
|
|
254
|
+
if (aligned > 0) return aligned;
|
|
255
|
+
const span = event.span.end - event.span.start;
|
|
256
|
+
if (span > 0) return span;
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
function alignedDuration(alignment) {
|
|
260
|
+
if (!alignment) return 0;
|
|
261
|
+
let ms = 0;
|
|
262
|
+
for (const duration of alignment.durationMs) ms += duration;
|
|
263
|
+
return ms;
|
|
264
|
+
}
|
|
235
265
|
function notePlayback(session, active, tag, offsetMs) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
session.captions.push({
|
|
242
|
-
type: "agent_spoken",
|
|
243
|
-
turnId: active.turnId,
|
|
244
|
-
utteranceId: active.utteranceId,
|
|
245
|
-
upTo
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (!("entryId" in tag.span)) return;
|
|
266
|
+
rememberPlay(active, tag, offsetMs);
|
|
267
|
+
const playing = session.clocks.get(tag);
|
|
268
|
+
if (playing) revealSpoken(session, active, playing, offsetMs);
|
|
269
|
+
const span = translateSpan(tag.span, session.maps);
|
|
270
|
+
if (!("entryId" in span)) return;
|
|
250
271
|
active.cut = {
|
|
251
|
-
entryId:
|
|
252
|
-
contentIndex:
|
|
253
|
-
heardUpTo:
|
|
272
|
+
entryId: span.entryId,
|
|
273
|
+
contentIndex: span.contentIndex,
|
|
274
|
+
heardUpTo: playedSource(span, playing, offsetMs),
|
|
254
275
|
heardMs: session.out.playedMs
|
|
255
276
|
};
|
|
256
277
|
}
|
|
278
|
+
function rememberPlay(active, tag, offsetMs) {
|
|
279
|
+
const existing = active.plays.find((play) => play.tag === tag);
|
|
280
|
+
if (existing) {
|
|
281
|
+
existing.offsetMs = offsetMs;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
active.plays.push({ tag, offsetMs });
|
|
285
|
+
}
|
|
286
|
+
function revealSpoken(session, active, playing, offsetMs) {
|
|
287
|
+
const upTo = utteranceIndex(spokenOffset(playing, offsetMs), session.maps);
|
|
288
|
+
if (upTo <= active.spokenUpTo) return;
|
|
289
|
+
active.spokenUpTo = upTo;
|
|
290
|
+
session.captions.push({
|
|
291
|
+
type: "agent_spoken",
|
|
292
|
+
turnId: active.turnId,
|
|
293
|
+
utteranceId: active.utteranceId,
|
|
294
|
+
upTo
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function playedSource(span, clock, offsetMs) {
|
|
298
|
+
if (!clock) return span.end;
|
|
299
|
+
const length = clock.end - clock.start;
|
|
300
|
+
if (length <= 0) return span.end;
|
|
301
|
+
const local = spokenOffset(clock, offsetMs) - clock.start;
|
|
302
|
+
return span.start + Math.round(local * (span.end - span.start) / length);
|
|
303
|
+
}
|
|
257
304
|
function translateSpan(span, maps) {
|
|
258
305
|
if ("entryId" in span) return span;
|
|
259
306
|
const map = answerMap(span, maps);
|
|
@@ -276,10 +323,11 @@ function scale(offset, spokenLen, sourceLen) {
|
|
|
276
323
|
if (spokenLen <= 0) return 0;
|
|
277
324
|
return Math.round(offset * sourceLen / spokenLen);
|
|
278
325
|
}
|
|
279
|
-
function bindEntry(maps, entryId) {
|
|
326
|
+
function bindEntry(session, active, maps, entryId) {
|
|
280
327
|
for (const map of maps) {
|
|
281
328
|
if (map.kind === "answer" && map.entryId === void 0) map.entryId = entryId;
|
|
282
329
|
}
|
|
330
|
+
for (const play of active.plays) notePlayback(session, active, play.tag, play.offsetMs);
|
|
283
331
|
}
|
|
284
332
|
function notePhase(setPhase, event) {
|
|
285
333
|
if (event.type === "run_start" || event.type === "turn_start") setPhase({ phase: "thinking" });
|
package/dist/turns.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 {\n type AudioSink,\n type AudioSource,\n type AudioTag,\n chunkSeconds,\n type TextSpan,\n} from './audio.ts';\nimport { type SpokenClock, spokenOffset, utteranceIndex } from './captions.ts';\nimport { type EchoControl, openEchoMic } from './echo.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { SpeechModel, TranscriptionModel } from './models.ts';\nimport { pushQueue } from './queue.ts';\nimport {\n type AudioOut,\n type BedFollower,\n followBed,\n isAudioOut,\n type Soundscape,\n} from './soundscape.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 echo?: EchoControl;\n soundscape?: Soundscape;\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 spokenUpTo: number;\n cut?: VoiceCut;\n}\n\nexport function voiceTurns(\n models: VoiceModels,\n conversation: VoiceConversation,\n mic: AudioSource,\n out: AudioSink | AudioOut,\n options: VoiceTurnOptions,\n signal: AbortSignal\n): VoiceSession {\n const phases = pushQueue<VoicePhase>();\n const captions = pushQueue<CaptionEvent>();\n const session: Session = {\n models,\n conversation,\n mic: heardMic(options, mic),\n out: speechOf(out),\n options,\n signal,\n phases,\n captions,\n maps: [],\n bed: attachBed(out, options.soundscape),\n };\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 maps: SpokenMap[];\n playing?: SpokenClock;\n bed: BedFollower;\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, offsetMs) => {\n if (!active) return;\n notePlayback(session, active, tag, offsetMs);\n });\n const setPhase = phaseSetter(session.phases, session.bed);\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 if (session.signal.aborted && active) await interrupt(session, active);\n else if (active) await active.done;\n stopPlayback();\n setPhase({ phase: 'idle' });\n session.bed.stop();\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 spokenUpTo: 0,\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 session.maps = maps;\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 playing: SpokenClock = {\n start: event.span.start,\n end: event.span.end,\n durationMs: Math.round(chunkSeconds(event.chunk) * 1000),\n };\n if (event.alignment) playing.alignment = event.alignment;\n session.playing = playing;\n const tag: AudioTag = { span };\n if (event.alignment) tag.alignment = event.alignment;\n session.out.write(event.chunk, tag);\n }\n}\n\nfunction notePlayback(session: Session, active: ActiveTurn, tag: AudioTag, offsetMs: number): void {\n const playing = session.playing;\n if (playing) {\n const upTo = utteranceIndex(spokenOffset(playing, offsetMs), session.maps);\n if (upTo > active.spokenUpTo) {\n active.spokenUpTo = upTo;\n session.captions.push({\n type: 'agent_spoken',\n turnId: active.turnId,\n utteranceId: active.utteranceId,\n upTo,\n });\n }\n }\n if (!('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\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 heardMic(options: VoiceTurnOptions, mic: AudioSource): AudioSource {\n const echo = options.echo;\n if (echo === undefined) return mic;\n return openEchoMic(echo, mic);\n}\n\nfunction speechOf(out: AudioSink | AudioOut): AudioSink {\n if (isAudioOut(out)) return out.speech;\n return out;\n}\n\nfunction attachBed(out: AudioSink | AudioOut, soundscape: Soundscape | undefined): BedFollower {\n if (!soundscape) return idleBed();\n if (!isAudioOut(out)) return idleBed();\n return followBed(out.bed, soundscape);\n}\n\nfunction idleBed(): BedFollower {\n return {\n observe() {},\n stop() {},\n };\n}\n\nfunction phaseSetter(phases: Session['phases'], bed: BedFollower): (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 bed.observe(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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",
|
|
4
|
+
"sourcesContent": ["import type {\n AssistantMessageEvent,\n JsonObject,\n ProviderRequestOptions,\n ToolCall,\n UserMessage,\n} from '@earendil-works/pi-ai';\nimport {\n type AudioSink,\n type AudioSource,\n type AudioTag,\n type CharAlignment,\n chunkSeconds,\n type TextSpan,\n} from './audio.ts';\nimport { type SpokenClock, spokenOffset, utteranceIndex } from './captions.ts';\nimport { type EchoControl, openEchoMic } from './echo.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { SpeechModel, TranscriptionModel } from './models.ts';\nimport { pushQueue } from './queue.ts';\nimport {\n type AudioOut,\n type BedFollower,\n followBed,\n isAudioOut,\n type Soundscape,\n} from './soundscape.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 echo?: EchoControl;\n soundscape?: Soundscape;\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 PlayedTag {\n tag: AudioTag;\n offsetMs: number;\n}\n\ninterface ActiveTurn {\n controller: AbortController;\n unlink: () => void;\n done: Promise<void>;\n turnId: string;\n utteranceId: string;\n spokenUpTo: number;\n plays: PlayedTag[];\n cut?: VoiceCut;\n}\n\nexport function voiceTurns(\n models: VoiceModels,\n conversation: VoiceConversation,\n mic: AudioSource,\n out: AudioSink | AudioOut,\n options: VoiceTurnOptions,\n signal: AbortSignal\n): VoiceSession {\n const phases = pushQueue<VoicePhase>();\n const captions = pushQueue<CaptionEvent>();\n const session: Session = {\n models,\n conversation,\n mic: heardMic(options, mic),\n out: speechOf(out),\n options,\n signal,\n phases,\n captions,\n maps: [],\n clocks: new WeakMap(),\n bed: attachBed(out, options.soundscape),\n };\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 maps: SpokenMap[];\n clocks: WeakMap<AudioTag, SpokenClock>;\n bed: BedFollower;\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, offsetMs) => {\n if (!active) return;\n notePlayback(session, active, tag, offsetMs);\n });\n const setPhase = phaseSetter(session.phases, session.bed);\n const turnFailed = Promise.withResolvers<never>();\n try {\n if (session.signal.aborted) return;\n setPhase({ phase: 'listening' });\n const heard = session.models\n .listen(session.options.ears, session.mic, {\n signal: session.signal,\n })\n [Symbol.asyncIterator]();\n while (true) {\n const event = await nextVoiceEvent(heard, turnFailed.promise);\n if (!event) break;\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, turnFailed.reject);\n }\n } finally {\n if (session.signal.aborted && active) await interrupt(session, active);\n else if (active) await active.done;\n stopPlayback();\n setPhase({ phase: 'idle' });\n session.bed.stop();\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\nasync function nextVoiceEvent(\n heard: AsyncIterator<TranscriptionEvent>,\n failed: Promise<never>\n): Promise<TranscriptionEvent | undefined> {\n const step = await Promise.race([heard.next(), failed]);\n if (step.done) return undefined;\n return step.value;\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 rejectTurn: (error: unknown) => void\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 spokenUpTo: 0,\n plays: [],\n done: Promise.resolve(),\n };\n active.done = performTurn(session, setPhase, active, event.text, event.at, meta).finally(() => {\n linked.unlink();\n });\n void active.done.catch((error: unknown) => rejectTurn(error));\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 session.maps = maps;\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(session, active, 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 = { span };\n if (event.alignment) tag.alignment = event.alignment;\n session.clocks.set(tag, clockOf(event));\n session.out.write(event.chunk, tag);\n }\n}\n\nfunction clockOf(event: Extract<SpeechEvent, { type: 'audio' }>): SpokenClock {\n const clock: SpokenClock = {\n start: event.span.start,\n end: event.span.end,\n durationMs: chunkDurationMs(event),\n };\n if (event.alignment) clock.alignment = event.alignment;\n return clock;\n}\n\nfunction chunkDurationMs(event: Extract<SpeechEvent, { type: 'audio' }>): number {\n const measured = Math.round(chunkSeconds(event.chunk) * 1000);\n if (measured > 0) return measured;\n const aligned = alignedDuration(event.alignment);\n if (aligned > 0) return aligned;\n const span = event.span.end - event.span.start;\n if (span > 0) return span;\n return 0;\n}\n\nfunction alignedDuration(alignment: CharAlignment | undefined): number {\n if (!alignment) return 0;\n let ms = 0;\n for (const duration of alignment.durationMs) ms += duration;\n return ms;\n}\n\nfunction notePlayback(session: Session, active: ActiveTurn, tag: AudioTag, offsetMs: number): void {\n rememberPlay(active, tag, offsetMs);\n const playing = session.clocks.get(tag);\n if (playing) revealSpoken(session, active, playing, offsetMs);\n const span = translateSpan(tag.span, session.maps);\n if (!('entryId' in span)) return;\n active.cut = {\n entryId: span.entryId,\n contentIndex: span.contentIndex,\n heardUpTo: playedSource(span, playing, offsetMs),\n heardMs: session.out.playedMs,\n };\n}\n\nfunction rememberPlay(active: ActiveTurn, tag: AudioTag, offsetMs: number) {\n const existing = active.plays.find((play) => play.tag === tag);\n if (existing) {\n existing.offsetMs = offsetMs;\n return;\n }\n active.plays.push({ tag, offsetMs });\n}\n\nfunction revealSpoken(\n session: Session,\n active: ActiveTurn,\n playing: SpokenClock,\n offsetMs: number\n) {\n const upTo = utteranceIndex(spokenOffset(playing, offsetMs), session.maps);\n if (upTo <= active.spokenUpTo) return;\n active.spokenUpTo = upTo;\n session.captions.push({\n type: 'agent_spoken',\n turnId: active.turnId,\n utteranceId: active.utteranceId,\n upTo,\n });\n}\n\nfunction playedSource(\n span: { start: number; end: number },\n clock: SpokenClock | undefined,\n offsetMs: number\n): number {\n if (!clock) return span.end;\n const length = clock.end - clock.start;\n if (length <= 0) return span.end;\n const local = spokenOffset(clock, offsetMs) - clock.start;\n return span.start + Math.round((local * (span.end - span.start)) / length);\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(session: Session, active: ActiveTurn, maps: SpokenMap[], entryId: string): void {\n for (const map of maps) {\n if (map.kind === 'answer' && map.entryId === undefined) map.entryId = entryId;\n }\n for (const play of active.plays) notePlayback(session, active, play.tag, play.offsetMs);\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 heardMic(options: VoiceTurnOptions, mic: AudioSource): AudioSource {\n const echo = options.echo;\n if (echo === undefined) return mic;\n return openEchoMic(echo, mic);\n}\n\nfunction speechOf(out: AudioSink | AudioOut): AudioSink {\n if (isAudioOut(out)) return out.speech;\n return out;\n}\n\nfunction attachBed(out: AudioSink | AudioOut, soundscape: Soundscape | undefined): BedFollower {\n if (!soundscape) return idleBed();\n if (!isAudioOut(out)) return idleBed();\n return followBed(out.bed, soundscape);\n}\n\nfunction idleBed(): BedFollower {\n return {\n observe() {},\n stop() {},\n };\n}\n\nfunction phaseSetter(phases: Session['phases'], bed: BedFollower): (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 bed.observe(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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JsonObject } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { AudioChunk } from '../audio.ts';
|
|
3
|
+
import type { RealtimeEvent } from '../realtime.ts';
|
|
4
|
+
export interface FishTurn {
|
|
5
|
+
dropAudio: boolean;
|
|
6
|
+
discardFinals: number;
|
|
7
|
+
speaking: boolean;
|
|
8
|
+
turnOpen: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface FishText {
|
|
11
|
+
identity: string;
|
|
12
|
+
text: string;
|
|
13
|
+
attributes: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export declare function ingestFishData(message: Record<string, unknown>): RealtimeEvent[];
|
|
16
|
+
export declare function ingestFishText(turn: FishTurn, packet: FishText, localIdentity: string): RealtimeEvent[];
|
|
17
|
+
export declare function ingestFishAudio(turn: FishTurn, chunk: AudioChunk): RealtimeEvent[];
|
|
18
|
+
export declare function ingestFishState(turn: FishTurn, value: string): RealtimeEvent[];
|
|
19
|
+
export declare function ingestSpeakers(turn: FishTurn, identities: string[]): RealtimeEvent[];
|
|
20
|
+
export declare function userMessage(value: string, speak: boolean): JsonObject;
|
|
21
|
+
export declare function toolResult(callId: string, result: string, isError: boolean): JsonObject;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RealtimeModel } from '../models.ts';
|
|
2
|
+
import type { FishOptions } from './session.ts';
|
|
3
|
+
export declare const fishAgent: RealtimeModel<'fish-agents'>;
|
|
4
|
+
export declare function registerFishAgents(options?: FishOptions): () => void;
|
|
5
|
+
export type { CustomLlm, HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
|
|
6
|
+
export { hashHostedSpec, memoryHostedCache } from '../hosted-agent.ts';
|
|
7
|
+
export type { ProvisionOptions } from './provision.ts';
|
|
8
|
+
export { agentConfig, httpsOrigin, provisionAgent } from './provision.ts';
|
|
9
|
+
export type { FishRoom, FishRoomFactory, FishSdk } from './room.ts';
|
|
10
|
+
export { importFishSdk, openFishRoom } from './room.ts';
|
|
11
|
+
export type { FishOptions } from './session.ts';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { JsonObject, ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import { type HostedAgent, type HostedAgentSpec, type HostedCache } from '../hosted-agent.ts';
|
|
3
|
+
import type { RealtimeModel } from '../models.ts';
|
|
4
|
+
export interface ProvisionOptions extends ProviderRequestOptions {
|
|
5
|
+
agentId?: string;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
cache?: HostedCache;
|
|
8
|
+
}
|
|
9
|
+
export declare function httpsOrigin(baseUrl: string): string;
|
|
10
|
+
export declare function agentConfig(spec: HostedAgentSpec, toolIds: string[]): JsonObject;
|
|
11
|
+
export declare function provisionAgent(model: RealtimeModel<string>, spec: HostedAgentSpec, options?: ProvisionOptions): Promise<HostedAgent>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AudioChunk, AudioSource } from '../audio.ts';
|
|
2
|
+
import type { MicOpen } from '../echo.ts';
|
|
3
|
+
export interface FishSdkRoom {
|
|
4
|
+
connect(url: string, token: string): Promise<void>;
|
|
5
|
+
disconnect(): void;
|
|
6
|
+
localParticipant: {
|
|
7
|
+
identity?: string;
|
|
8
|
+
publishTrack(track: unknown, options?: unknown): Promise<unknown>;
|
|
9
|
+
publishData(data: Uint8Array, options: {
|
|
10
|
+
reliable: boolean;
|
|
11
|
+
topic: string;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
15
|
+
registerTextStreamHandler(topic: string, handler: (reader: {
|
|
16
|
+
readAll(): Promise<string>;
|
|
17
|
+
info: {
|
|
18
|
+
attributes?: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
}, participant: {
|
|
21
|
+
identity: string;
|
|
22
|
+
}) => void): void;
|
|
23
|
+
}
|
|
24
|
+
export interface FishSdk {
|
|
25
|
+
Room: new () => FishSdkRoom;
|
|
26
|
+
}
|
|
27
|
+
export interface FishHandlers {
|
|
28
|
+
data(topic: string, json: unknown, identity: string | undefined): void;
|
|
29
|
+
text(identity: string, value: string, attributes: Record<string, string>): void;
|
|
30
|
+
attribute(key: string, value: string): void;
|
|
31
|
+
speakers(identities: string[]): void;
|
|
32
|
+
audio(chunk: AudioChunk): void;
|
|
33
|
+
fail(error: unknown): void;
|
|
34
|
+
}
|
|
35
|
+
export interface FishRoom {
|
|
36
|
+
url?: string;
|
|
37
|
+
token?: string;
|
|
38
|
+
identity: string;
|
|
39
|
+
connect(url: string, token: string): Promise<void>;
|
|
40
|
+
publishMic(source: AudioSource, open: MicOpen): Promise<void>;
|
|
41
|
+
publishData(payload: string): Promise<void>;
|
|
42
|
+
close(): void;
|
|
43
|
+
}
|
|
44
|
+
export type FishRoomFactory = (handlers: FishHandlers) => FishRoom;
|
|
45
|
+
export declare function importFishSdk(load?: (specifier: string) => Promise<FishSdk>): Promise<FishSdk>;
|
|
46
|
+
export declare function openFishRoom(sdk: FishSdk, handlers: FishHandlers): FishRoom;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AudioSource } from '../audio.ts';
|
|
2
|
+
import { type EchoControl } from '../echo.ts';
|
|
3
|
+
import type { HostedAgentSpec, HostedCache } from '../hosted-agent.ts';
|
|
4
|
+
import type { RealtimeModel } from '../models.ts';
|
|
5
|
+
import type { RealtimeContext, RealtimeOptions, RealtimeSession } from '../realtime.ts';
|
|
6
|
+
import { type FishRoomFactory, type FishSdk } from './room.ts';
|
|
7
|
+
export interface FishOptions {
|
|
8
|
+
room?: FishRoomFactory;
|
|
9
|
+
sdk?: FishSdk;
|
|
10
|
+
cache?: HostedCache;
|
|
11
|
+
agentId?: string;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
firstMessage?: string;
|
|
14
|
+
microphone?: AudioSource;
|
|
15
|
+
echo?: EchoControl;
|
|
16
|
+
loadSdk?: (specifier: string) => Promise<FishSdk>;
|
|
17
|
+
}
|
|
18
|
+
export declare function specFromContext(model: RealtimeModel<string>, context: RealtimeContext): HostedAgentSpec;
|
|
19
|
+
export declare function connectFish(model: RealtimeModel<string>, context: RealtimeContext, options: RealtimeOptions | undefined, settings?: FishOptions): Promise<RealtimeSession>;
|