@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const loadSpecifier = new Function("specifier", "return import(specifier)");
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
async function importFishSdk(load = loadSpecifier) {
|
|
6
|
+
try {
|
|
7
|
+
return await load("livekit-client");
|
|
8
|
+
} catch (browserError) {
|
|
9
|
+
try {
|
|
10
|
+
return await load("@livekit/rtc-node");
|
|
11
|
+
} catch {
|
|
12
|
+
if (browserError instanceof Error) throw browserError;
|
|
13
|
+
throw new Error(String(browserError));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function decodePacket(payload) {
|
|
18
|
+
const raw = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
function openFishRoom(sdk, handlers) {
|
|
22
|
+
const room = new sdk.Room();
|
|
23
|
+
room.on("dataReceived", (payload, participant, _kind, topic) => {
|
|
24
|
+
if (topic !== "agent-event") return;
|
|
25
|
+
const identity = isRecord(participant) ? text(participant.identity) : void 0;
|
|
26
|
+
try {
|
|
27
|
+
handlers.data(String(topic), decodePacket(payload), identity);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
handlers.fail(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
room.registerTextStreamHandler("lk.transcription", (reader, participant) => {
|
|
33
|
+
void reader.readAll().then(
|
|
34
|
+
(value) => {
|
|
35
|
+
handlers.text(participant.identity, value, reader.info.attributes ?? {});
|
|
36
|
+
},
|
|
37
|
+
(error) => handlers.fail(error)
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
room.on("participantAttributesChanged", (changed, _participant) => {
|
|
41
|
+
if (!isRecord(changed)) return;
|
|
42
|
+
const value = changed["lk.agent.state"];
|
|
43
|
+
if (typeof value === "string") handlers.attribute("lk.agent.state", value);
|
|
44
|
+
});
|
|
45
|
+
room.on("activeSpeakersChanged", (speakers) => {
|
|
46
|
+
const identities = Array.isArray(speakers) ? speakers.filter(isRecord).map((speaker) => text(speaker.identity)) : [];
|
|
47
|
+
handlers.speakers(identities);
|
|
48
|
+
});
|
|
49
|
+
room.on("trackSubscribed", (track) => {
|
|
50
|
+
if (!isRecord(track) || typeof track.on !== "function") return;
|
|
51
|
+
track.on("audio", (chunk) => handlers.audio(chunk));
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
identity: "",
|
|
55
|
+
async connect(url, token) {
|
|
56
|
+
await room.connect(url, token);
|
|
57
|
+
this.identity = room.localParticipant.identity ?? "";
|
|
58
|
+
},
|
|
59
|
+
async publishMic(source, open) {
|
|
60
|
+
await room.localParticipant.publishTrack(source, { echoCancellation: open.echoCancellation });
|
|
61
|
+
},
|
|
62
|
+
async publishData(payload) {
|
|
63
|
+
await room.localParticipant.publishData(new TextEncoder().encode(payload), {
|
|
64
|
+
reliable: true,
|
|
65
|
+
topic: "client-event"
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
close() {
|
|
69
|
+
room.disconnect();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function text(value) {
|
|
74
|
+
return typeof value === "string" ? value : "";
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
importFishSdk,
|
|
78
|
+
openFishRoom
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=room.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish-agents/room.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioChunk, AudioSource } from '../audio.ts';\nimport type { MicOpen } from '../echo.ts';\n\nconst loadSpecifier = new Function('specifier', 'return import(specifier)') as (\n specifier: string\n) => Promise<FishSdk>;\n\nexport interface FishSdkRoom {\n connect(url: string, token: string): Promise<void>;\n disconnect(): void;\n localParticipant: {\n identity?: string;\n publishTrack(track: unknown, options?: unknown): Promise<unknown>;\n publishData(data: Uint8Array, options: { reliable: boolean; topic: string }): Promise<void>;\n };\n on(event: string, listener: (...args: unknown[]) => void): void;\n registerTextStreamHandler(\n topic: string,\n handler: (\n reader: { readAll(): Promise<string>; info: { attributes?: Record<string, string> } },\n participant: { identity: string }\n ) => void\n ): void;\n}\n\nexport interface FishSdk {\n Room: new () => FishSdkRoom;\n}\n\nexport interface FishHandlers {\n data(topic: string, json: unknown, identity: string | undefined): void;\n text(identity: string, value: string, attributes: Record<string, string>): void;\n attribute(key: string, value: string): void;\n speakers(identities: string[]): void;\n audio(chunk: AudioChunk): void;\n fail(error: unknown): void;\n}\n\nexport interface FishRoom {\n url?: string;\n token?: string;\n identity: string;\n connect(url: string, token: string): Promise<void>;\n publishMic(source: AudioSource, open: MicOpen): Promise<void>;\n publishData(payload: string): Promise<void>;\n close(): void;\n}\n\nexport type FishRoomFactory = (handlers: FishHandlers) => FishRoom;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport async function importFishSdk(\n load: (specifier: string) => Promise<FishSdk> = loadSpecifier\n): Promise<FishSdk> {\n try {\n return await load('livekit-client');\n } catch (browserError) {\n try {\n return await load('@livekit/rtc-node');\n } catch {\n if (browserError instanceof Error) throw browserError;\n throw new Error(String(browserError));\n }\n }\n}\n\nfunction decodePacket(payload: unknown): unknown {\n const raw =\n typeof payload === 'string' ? payload : new TextDecoder().decode(payload as Uint8Array);\n return JSON.parse(raw);\n}\n\nexport function openFishRoom(sdk: FishSdk, handlers: FishHandlers): FishRoom {\n const room = new sdk.Room();\n room.on('dataReceived', (payload, participant, _kind, topic) => {\n if (topic !== 'agent-event') return;\n const identity = isRecord(participant) ? text(participant.identity) : undefined;\n try {\n handlers.data(String(topic), decodePacket(payload), identity);\n } catch (error) {\n handlers.fail(error);\n }\n });\n room.registerTextStreamHandler('lk.transcription', (reader, participant) => {\n void reader.readAll().then(\n (value) => {\n handlers.text(participant.identity, value, reader.info.attributes ?? {});\n },\n (error: unknown) => handlers.fail(error)\n );\n });\n room.on('participantAttributesChanged', (changed, _participant) => {\n if (!isRecord(changed)) return;\n const value = changed['lk.agent.state'];\n if (typeof value === 'string') handlers.attribute('lk.agent.state', value);\n });\n room.on('activeSpeakersChanged', (speakers) => {\n const identities = Array.isArray(speakers)\n ? speakers.filter(isRecord).map((speaker) => text(speaker.identity))\n : [];\n handlers.speakers(identities);\n });\n room.on('trackSubscribed', (track) => {\n if (!isRecord(track) || typeof track.on !== 'function') return;\n track.on('audio', (chunk: AudioChunk) => handlers.audio(chunk));\n });\n return {\n identity: '',\n async connect(url, token) {\n await room.connect(url, token);\n this.identity = room.localParticipant.identity ?? '';\n },\n async publishMic(source, open) {\n await room.localParticipant.publishTrack(source, { echoCancellation: open.echoCancellation });\n },\n async publishData(payload) {\n await room.localParticipant.publishData(new TextEncoder().encode(payload), {\n reliable: true,\n topic: 'client-event',\n });\n },\n close() {\n room.disconnect();\n },\n };\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n"],
|
|
5
|
+
"mappings": "AAGA,MAAM,gBAAgB,IAAI,SAAS,aAAa,0BAA0B;AA+C1E,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,eAAsB,cACpB,OAAgD,eAC9B;AAClB,MAAI;AACF,WAAO,MAAM,KAAK,gBAAgB;AAAA,EACpC,SAAS,cAAc;AACrB,QAAI;AACF,aAAO,MAAM,KAAK,mBAAmB;AAAA,IACvC,QAAQ;AACN,UAAI,wBAAwB,MAAO,OAAM;AACzC,YAAM,IAAI,MAAM,OAAO,YAAY,CAAC;AAAA,IACtC;AAAA,EACF;AACF;AAEA,SAAS,aAAa,SAA2B;AAC/C,QAAM,MACJ,OAAO,YAAY,WAAW,UAAU,IAAI,YAAY,EAAE,OAAO,OAAqB;AACxF,SAAO,KAAK,MAAM,GAAG;AACvB;AAEO,SAAS,aAAa,KAAc,UAAkC;AAC3E,QAAM,OAAO,IAAI,IAAI,KAAK;AAC1B,OAAK,GAAG,gBAAgB,CAAC,SAAS,aAAa,OAAO,UAAU;AAC9D,QAAI,UAAU,cAAe;AAC7B,UAAM,WAAW,SAAS,WAAW,IAAI,KAAK,YAAY,QAAQ,IAAI;AACtE,QAAI;AACF,eAAS,KAAK,OAAO,KAAK,GAAG,aAAa,OAAO,GAAG,QAAQ;AAAA,IAC9D,SAAS,OAAO;AACd,eAAS,KAAK,KAAK;AAAA,IACrB;AAAA,EACF,CAAC;AACD,OAAK,0BAA0B,oBAAoB,CAAC,QAAQ,gBAAgB;AAC1E,SAAK,OAAO,QAAQ,EAAE;AAAA,MACpB,CAAC,UAAU;AACT,iBAAS,KAAK,YAAY,UAAU,OAAO,OAAO,KAAK,cAAc,CAAC,CAAC;AAAA,MACzE;AAAA,MACA,CAAC,UAAmB,SAAS,KAAK,KAAK;AAAA,IACzC;AAAA,EACF,CAAC;AACD,OAAK,GAAG,gCAAgC,CAAC,SAAS,iBAAiB;AACjE,QAAI,CAAC,SAAS,OAAO,EAAG;AACxB,UAAM,QAAQ,QAAQ,gBAAgB;AACtC,QAAI,OAAO,UAAU,SAAU,UAAS,UAAU,kBAAkB,KAAK;AAAA,EAC3E,CAAC;AACD,OAAK,GAAG,yBAAyB,CAAC,aAAa;AAC7C,UAAM,aAAa,MAAM,QAAQ,QAAQ,IACrC,SAAS,OAAO,QAAQ,EAAE,IAAI,CAAC,YAAY,KAAK,QAAQ,QAAQ,CAAC,IACjE,CAAC;AACL,aAAS,SAAS,UAAU;AAAA,EAC9B,CAAC;AACD,OAAK,GAAG,mBAAmB,CAAC,UAAU;AACpC,QAAI,CAAC,SAAS,KAAK,KAAK,OAAO,MAAM,OAAO,WAAY;AACxD,UAAM,GAAG,SAAS,CAAC,UAAsB,SAAS,MAAM,KAAK,CAAC;AAAA,EAChE,CAAC;AACD,SAAO;AAAA,IACL,UAAU;AAAA,IACV,MAAM,QAAQ,KAAK,OAAO;AACxB,YAAM,KAAK,QAAQ,KAAK,KAAK;AAC7B,WAAK,WAAW,KAAK,iBAAiB,YAAY;AAAA,IACpD;AAAA,IACA,MAAM,WAAW,QAAQ,MAAM;AAC7B,YAAM,KAAK,iBAAiB,aAAa,QAAQ,EAAE,kBAAkB,KAAK,iBAAiB,CAAC;AAAA,IAC9F;AAAA,IACA,MAAM,YAAY,SAAS;AACzB,YAAM,KAAK,iBAAiB,YAAY,IAAI,YAAY,EAAE,OAAO,OAAO,GAAG;AAAA,QACzE,UAAU;AAAA,QACV,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,IACA,QAAQ;AACN,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AACF;AAEA,SAAS,KAAK,OAAwB;AACpC,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { micOpen, openEchoMic } from "../echo.js";
|
|
2
|
+
import { VoiceRequestError } from "../errors.js";
|
|
3
|
+
import { pushQueue } from "../queue.js";
|
|
4
|
+
import { planTruncation, RealtimeLedger } from "../realtime.js";
|
|
5
|
+
import { priceAudio } from "../usage.js";
|
|
6
|
+
import {
|
|
7
|
+
ingestFishAudio,
|
|
8
|
+
ingestFishData,
|
|
9
|
+
ingestFishState,
|
|
10
|
+
ingestFishText,
|
|
11
|
+
ingestSpeakers,
|
|
12
|
+
toolResult,
|
|
13
|
+
userMessage
|
|
14
|
+
} from "./events.js";
|
|
15
|
+
import { provisionAgent } from "./provision.js";
|
|
16
|
+
import {
|
|
17
|
+
importFishSdk,
|
|
18
|
+
openFishRoom
|
|
19
|
+
} from "./room.js";
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function resultText(result) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
for (const part of result.content) {
|
|
26
|
+
if (part.type === "text") parts.push(part.text);
|
|
27
|
+
}
|
|
28
|
+
return parts.join("\n");
|
|
29
|
+
}
|
|
30
|
+
function hasKey(options) {
|
|
31
|
+
return typeof options?.apiKey === "string" && options.apiKey.length > 0;
|
|
32
|
+
}
|
|
33
|
+
function talkerLlm(model) {
|
|
34
|
+
const cascade = model.cascade;
|
|
35
|
+
if (cascade === void 0) return "fish";
|
|
36
|
+
return cascade.llm;
|
|
37
|
+
}
|
|
38
|
+
function specFromContext(model, context) {
|
|
39
|
+
return {
|
|
40
|
+
name: model.name,
|
|
41
|
+
instructions: context.instructions,
|
|
42
|
+
tools: context.tools,
|
|
43
|
+
voice: context.voice,
|
|
44
|
+
llm: talkerLlm(model)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function placeholder(agentId) {
|
|
48
|
+
return { agentId, specHash: "", publishedAt: 0 };
|
|
49
|
+
}
|
|
50
|
+
async function ensureAgent(model, context, options, settings) {
|
|
51
|
+
if (!hasKey(options)) {
|
|
52
|
+
const id = settings.agentId;
|
|
53
|
+
if (id === void 0 || id.length === 0) {
|
|
54
|
+
throw new VoiceRequestError("error", "missing realtime credential");
|
|
55
|
+
}
|
|
56
|
+
return placeholder(id);
|
|
57
|
+
}
|
|
58
|
+
const provision = {
|
|
59
|
+
...options,
|
|
60
|
+
agentId: settings.agentId,
|
|
61
|
+
cache: settings.cache,
|
|
62
|
+
now: settings.now
|
|
63
|
+
};
|
|
64
|
+
return provisionAgent(model, specFromContext(model, context), provision);
|
|
65
|
+
}
|
|
66
|
+
function sessionBody(agentId, context, firstMessage) {
|
|
67
|
+
const overrides = {};
|
|
68
|
+
if (context.instructions.length > 0) overrides.system_prompt = context.instructions;
|
|
69
|
+
if (context.voice.length > 0) overrides.voice_id = context.voice;
|
|
70
|
+
if (firstMessage !== void 0) overrides.first_message = firstMessage;
|
|
71
|
+
const body = { agent_id: agentId };
|
|
72
|
+
if (Object.keys(overrides).length > 0) body.overrides = overrides;
|
|
73
|
+
return body;
|
|
74
|
+
}
|
|
75
|
+
function authHeaders(options) {
|
|
76
|
+
const headers = { "Content-Type": "application/json" };
|
|
77
|
+
const key = options?.apiKey;
|
|
78
|
+
if (typeof key === "string" && key.length > 0) headers.Authorization = `Bearer ${key}`;
|
|
79
|
+
const extra = options?.headers;
|
|
80
|
+
if (extra === void 0) return headers;
|
|
81
|
+
for (const [key2, value] of Object.entries(extra)) {
|
|
82
|
+
if (value === null) delete headers[key2];
|
|
83
|
+
else headers[key2] = value;
|
|
84
|
+
}
|
|
85
|
+
return headers;
|
|
86
|
+
}
|
|
87
|
+
async function createSession(model, agentId, context, options, firstMessage) {
|
|
88
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
89
|
+
const response = await fetchImpl(`${new URL(model.baseUrl).origin}/v1/agent/sessions`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: authHeaders(options),
|
|
92
|
+
body: JSON.stringify(sessionBody(agentId, context, firstMessage)),
|
|
93
|
+
signal: options?.signal
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
throw new VoiceRequestError("error", `session request failed (${response.status})`);
|
|
97
|
+
const json = await response.json();
|
|
98
|
+
if (!isRecord(json)) throw new VoiceRequestError("error", "session response was not an object");
|
|
99
|
+
if (json.transport !== "livekit") throw new VoiceRequestError("error", "unsupported transport");
|
|
100
|
+
const url = json.livekit_url;
|
|
101
|
+
const token = json.token;
|
|
102
|
+
const sessionId = json.session_id;
|
|
103
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
104
|
+
throw new VoiceRequestError("error", "missing livekit url");
|
|
105
|
+
}
|
|
106
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
107
|
+
throw new VoiceRequestError("error", "missing livekit token");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
url,
|
|
111
|
+
token,
|
|
112
|
+
sessionId: typeof sessionId === "string" && sessionId.length > 0 ? sessionId : "session"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function joinTarget(model, context, options, settings) {
|
|
116
|
+
const minted = options?.token;
|
|
117
|
+
if (minted !== void 0 && minted.value.length > 0) {
|
|
118
|
+
const url = minted.url;
|
|
119
|
+
if (url === void 0 || url.length === 0)
|
|
120
|
+
throw new VoiceRequestError("error", "missing livekit url");
|
|
121
|
+
return { url, token: minted.value, sessionId: "session" };
|
|
122
|
+
}
|
|
123
|
+
const agent = await ensureAgent(model, context, options, settings);
|
|
124
|
+
return createSession(model, agent.agentId, context, options, settings.firstMessage);
|
|
125
|
+
}
|
|
126
|
+
async function openRoom(settings, handlers) {
|
|
127
|
+
if (settings.room !== void 0) return settings.room(handlers);
|
|
128
|
+
if (settings.sdk !== void 0) return openFishRoom(settings.sdk, handlers);
|
|
129
|
+
return openFishRoom(await importFishSdk(settings.loadSdk), handlers);
|
|
130
|
+
}
|
|
131
|
+
async function publishMicrophone(room, settings) {
|
|
132
|
+
const mic = settings.microphone;
|
|
133
|
+
if (mic === void 0) return;
|
|
134
|
+
const echo = settings.echo;
|
|
135
|
+
const mode = echo === void 0 ? "browser" : echo.mode;
|
|
136
|
+
const source = echo === void 0 ? mic : openEchoMic(echo, mic);
|
|
137
|
+
await room.publishMic(source, micOpen(mode, false));
|
|
138
|
+
}
|
|
139
|
+
class FishSession {
|
|
140
|
+
model;
|
|
141
|
+
#context;
|
|
142
|
+
#room;
|
|
143
|
+
#now;
|
|
144
|
+
#started;
|
|
145
|
+
#ledger = new RealtimeLedger();
|
|
146
|
+
#events = pushQueue();
|
|
147
|
+
#turn = {
|
|
148
|
+
dropAudio: false,
|
|
149
|
+
discardFinals: 0,
|
|
150
|
+
speaking: false,
|
|
151
|
+
turnOpen: false
|
|
152
|
+
};
|
|
153
|
+
#closed = false;
|
|
154
|
+
#usage;
|
|
155
|
+
constructor(model, context, room, now) {
|
|
156
|
+
this.model = model;
|
|
157
|
+
this.#context = context;
|
|
158
|
+
this.#room = room;
|
|
159
|
+
this.#now = now;
|
|
160
|
+
this.#started = now();
|
|
161
|
+
}
|
|
162
|
+
start(sessionId) {
|
|
163
|
+
this.#emit({ type: "session_start", sessionId });
|
|
164
|
+
}
|
|
165
|
+
#assertOpen() {
|
|
166
|
+
if (this.#closed) throw new VoiceRequestError("error", "session is closed");
|
|
167
|
+
}
|
|
168
|
+
#send(event) {
|
|
169
|
+
void Promise.resolve(this.#room.publishData(JSON.stringify(event))).catch((error) => {
|
|
170
|
+
this.#fail(error);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
#emit(event) {
|
|
174
|
+
if (this.#closed) return;
|
|
175
|
+
this.#ledger.observe(event);
|
|
176
|
+
this.#events.push(event);
|
|
177
|
+
}
|
|
178
|
+
appendAudio(chunk) {
|
|
179
|
+
this.#assertOpen();
|
|
180
|
+
this.#ledger.addInput(chunk);
|
|
181
|
+
const source = (async function* source2() {
|
|
182
|
+
yield chunk;
|
|
183
|
+
})();
|
|
184
|
+
void publishMicrophone(this.#room, { microphone: source }).catch(
|
|
185
|
+
(error) => this.#fail(error)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
sendText(value) {
|
|
189
|
+
this.#assertOpen();
|
|
190
|
+
this.#ledger.addText();
|
|
191
|
+
this.#send(userMessage(value, true));
|
|
192
|
+
}
|
|
193
|
+
inject(value, delivery) {
|
|
194
|
+
this.#assertOpen();
|
|
195
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
196
|
+
this.#assertOpen();
|
|
197
|
+
this.#ledger.addText();
|
|
198
|
+
if (delivery === "silent") this.#turn.discardFinals += 1;
|
|
199
|
+
if (delivery === "interrupt") this.#send({ type: "user.interrupt" });
|
|
200
|
+
this.#send(userMessage(value, delivery !== "silent"));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
respond(callId, result, delivery) {
|
|
204
|
+
this.#assertOpen();
|
|
205
|
+
const send = () => {
|
|
206
|
+
this.#send(toolResult(callId, resultText(result), result.isError));
|
|
207
|
+
};
|
|
208
|
+
if (delivery === void 0) {
|
|
209
|
+
send();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.#ledger.submit(this.model.delivery, delivery, () => {
|
|
213
|
+
this.#assertOpen();
|
|
214
|
+
send();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
interrupt(heard) {
|
|
218
|
+
this.#assertOpen();
|
|
219
|
+
planTruncation(this.model.truncation, heard);
|
|
220
|
+
this.#turn.dropAudio = true;
|
|
221
|
+
this.#send({ type: "user.interrupt" });
|
|
222
|
+
this.#ledger.settle();
|
|
223
|
+
}
|
|
224
|
+
update(patch) {
|
|
225
|
+
this.#assertOpen();
|
|
226
|
+
if (patch.tools !== void 0 && this.model.liveToolUpdate === false) {
|
|
227
|
+
throw new VoiceRequestError("error", "live tool update is unsupported");
|
|
228
|
+
}
|
|
229
|
+
if (patch.tools !== void 0) this.#context.tools = patch.tools;
|
|
230
|
+
if (patch.instructions !== void 0) this.#context.instructions = patch.instructions;
|
|
231
|
+
if (patch.voice !== void 0) this.#context.voice = patch.voice;
|
|
232
|
+
}
|
|
233
|
+
async close() {
|
|
234
|
+
if (this.#usage !== void 0) return { usage: this.#usage };
|
|
235
|
+
this.#ledger.settle();
|
|
236
|
+
this.#closed = true;
|
|
237
|
+
this.#room.close();
|
|
238
|
+
this.#events.end();
|
|
239
|
+
const inputSeconds = Math.max(0, this.#now() - this.#started) / 1e3;
|
|
240
|
+
this.#usage = priceAudio(this.model.audioCost, {
|
|
241
|
+
inputSeconds,
|
|
242
|
+
outputSeconds: this.#ledger.outputSeconds,
|
|
243
|
+
textMessages: this.#ledger.textMessages
|
|
244
|
+
});
|
|
245
|
+
return { usage: this.#usage };
|
|
246
|
+
}
|
|
247
|
+
[Symbol.asyncIterator]() {
|
|
248
|
+
return this.#events.iterable[Symbol.asyncIterator]();
|
|
249
|
+
}
|
|
250
|
+
handlers() {
|
|
251
|
+
return {
|
|
252
|
+
data: (topic, json) => {
|
|
253
|
+
if (topic !== "agent-event" || !isRecord(json)) return;
|
|
254
|
+
for (const event of ingestFishData(json)) this.#emit(event);
|
|
255
|
+
},
|
|
256
|
+
text: (identity, value, attributes) => {
|
|
257
|
+
const packet = { identity, text: value, attributes };
|
|
258
|
+
for (const event of ingestFishText(this.#turn, packet, this.#room.identity))
|
|
259
|
+
this.#emit(event);
|
|
260
|
+
},
|
|
261
|
+
attribute: (key, value) => {
|
|
262
|
+
if (key !== "lk.agent.state") return;
|
|
263
|
+
for (const event of ingestFishState(this.#turn, value)) this.#emit(event);
|
|
264
|
+
},
|
|
265
|
+
speakers: (identities) => {
|
|
266
|
+
for (const event of ingestSpeakers(this.#turn, identities)) this.#emit(event);
|
|
267
|
+
},
|
|
268
|
+
audio: (chunk) => {
|
|
269
|
+
for (const event of ingestFishAudio(this.#turn, chunk)) this.#emit(event);
|
|
270
|
+
},
|
|
271
|
+
fail: (error) => this.#fail(error)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
#fail(error) {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
this.#emit({ type: "error", error: message, fatal: true });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function connectFish(model, context, options, settings = {}) {
|
|
280
|
+
if (options?.signal?.aborted === true) throw new VoiceRequestError("aborted", "session aborted");
|
|
281
|
+
const target = await joinTarget(model, context, options, settings);
|
|
282
|
+
const sessionHolder = {};
|
|
283
|
+
const room = await openRoom(settings, {
|
|
284
|
+
data: (topic, json) => sessionHolder.session?.handlers().data(topic, json, void 0),
|
|
285
|
+
text: (identity, value, attributes) => sessionHolder.session?.handlers().text(identity, value, attributes),
|
|
286
|
+
attribute: (key, value) => sessionHolder.session?.handlers().attribute(key, value),
|
|
287
|
+
speakers: (identities) => sessionHolder.session?.handlers().speakers(identities),
|
|
288
|
+
audio: (chunk) => sessionHolder.session?.handlers().audio(chunk),
|
|
289
|
+
fail: (error) => sessionHolder.session?.handlers().fail(error)
|
|
290
|
+
});
|
|
291
|
+
const now = settings.now !== void 0 ? settings.now : Date.now;
|
|
292
|
+
const session = new FishSession(model, context, room, now);
|
|
293
|
+
sessionHolder.session = session;
|
|
294
|
+
await room.connect(target.url, target.token);
|
|
295
|
+
if (room.identity.length === 0) room.identity = "user";
|
|
296
|
+
await publishMicrophone(room, settings);
|
|
297
|
+
session.start(target.sessionId);
|
|
298
|
+
return session;
|
|
299
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
connectFish,
|
|
302
|
+
specFromContext
|
|
303
|
+
};
|
|
304
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish-agents/session.ts"],
|
|
4
|
+
"sourcesContent": ["import type { JsonObject, ToolResultMessage, Usage } from '@earendil-works/pi-ai';\nimport type { AudioChunk, AudioSource } from '../audio.ts';\nimport { type EchoControl, micOpen, openEchoMic } from '../echo.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { HostedAgent, HostedAgentSpec, HostedCache } from '../hosted-agent.ts';\nimport type { Delivery, RealtimeModel } from '../models.ts';\nimport { pushQueue } from '../queue.ts';\nimport type {\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n} from '../realtime.ts';\nimport { planTruncation, RealtimeLedger } from '../realtime.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n type FishText,\n type FishTurn,\n ingestFishAudio,\n ingestFishData,\n ingestFishState,\n ingestFishText,\n ingestSpeakers,\n toolResult,\n userMessage,\n} from './events.ts';\nimport { type ProvisionOptions, provisionAgent } from './provision.ts';\nimport {\n type FishHandlers,\n type FishRoom,\n type FishRoomFactory,\n type FishSdk,\n importFishSdk,\n openFishRoom,\n} from './room.ts';\n\nexport interface FishOptions {\n room?: FishRoomFactory;\n sdk?: FishSdk;\n cache?: HostedCache;\n agentId?: string;\n now?: () => number;\n firstMessage?: string;\n microphone?: AudioSource;\n echo?: EchoControl;\n loadSdk?: (specifier: string) => Promise<FishSdk>;\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 hasKey(options: RealtimeOptions | undefined): boolean {\n return typeof options?.apiKey === 'string' && options.apiKey.length > 0;\n}\n\nfunction talkerLlm(model: RealtimeModel<string>): string {\n const cascade = model.cascade;\n if (cascade === undefined) return 'fish';\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 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: FishOptions\n): Promise<HostedAgent> {\n if (!hasKey(options)) {\n const id = settings.agentId;\n if (id === undefined || id.length === 0) {\n throw new VoiceRequestError('error', 'missing realtime credential');\n }\n return placeholder(id);\n }\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\nfunction sessionBody(\n agentId: string,\n context: RealtimeContext,\n firstMessage: string | undefined\n): JsonObject {\n const overrides: JsonObject = {};\n if (context.instructions.length > 0) overrides.system_prompt = context.instructions;\n if (context.voice.length > 0) overrides.voice_id = context.voice;\n if (firstMessage !== undefined) overrides.first_message = firstMessage;\n const body: JsonObject = { agent_id: agentId };\n if (Object.keys(overrides).length > 0) body.overrides = overrides;\n return body;\n}\n\nfunction authHeaders(options: RealtimeOptions | undefined): Record<string, string> {\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n const key = options?.apiKey;\n if (typeof key === 'string' && key.length > 0) headers.Authorization = `Bearer ${key}`;\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\nasync function createSession(\n model: RealtimeModel<string>,\n agentId: string,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n firstMessage: string | undefined\n): Promise<{ url: string; token: string; sessionId: string }> {\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const response = await fetchImpl(`${new URL(model.baseUrl).origin}/v1/agent/sessions`, {\n method: 'POST',\n headers: authHeaders(options),\n body: JSON.stringify(sessionBody(agentId, context, firstMessage)),\n signal: options?.signal,\n });\n if (!response.ok)\n throw new VoiceRequestError('error', `session request failed (${response.status})`);\n const json: unknown = await response.json();\n if (!isRecord(json)) throw new VoiceRequestError('error', 'session response was not an object');\n if (json.transport !== 'livekit') throw new VoiceRequestError('error', 'unsupported transport');\n const url = json.livekit_url;\n const token = json.token;\n const sessionId = json.session_id;\n if (typeof url !== 'string' || url.length === 0) {\n throw new VoiceRequestError('error', 'missing livekit url');\n }\n if (typeof token !== 'string' || token.length === 0) {\n throw new VoiceRequestError('error', 'missing livekit token');\n }\n return {\n url,\n token,\n sessionId: typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : 'session',\n };\n}\n\nasync function joinTarget(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: FishOptions\n): Promise<{ url: string; token: string; sessionId: string }> {\n const minted = options?.token;\n if (minted !== undefined && minted.value.length > 0) {\n const url = minted.url;\n if (url === undefined || url.length === 0)\n throw new VoiceRequestError('error', 'missing livekit url');\n return { url, token: minted.value, sessionId: 'session' };\n }\n const agent = await ensureAgent(model, context, options, settings);\n return createSession(model, agent.agentId, context, options, settings.firstMessage);\n}\n\nasync function openRoom(settings: FishOptions, handlers: FishHandlers): Promise<FishRoom> {\n if (settings.room !== undefined) return settings.room(handlers);\n if (settings.sdk !== undefined) return openFishRoom(settings.sdk, handlers);\n return openFishRoom(await importFishSdk(settings.loadSdk), handlers);\n}\n\nasync function publishMicrophone(room: FishRoom, settings: FishOptions): Promise<void> {\n const mic = settings.microphone;\n if (mic === undefined) return;\n const echo = settings.echo;\n const mode = echo === undefined ? 'browser' : echo.mode;\n const source = echo === undefined ? mic : openEchoMic(echo, mic);\n await room.publishMic(source, micOpen(mode, false));\n}\n\nclass FishSession implements RealtimeSession {\n readonly model: RealtimeModel<string>;\n readonly #context: RealtimeContext;\n readonly #room: FishRoom;\n readonly #now: () => number;\n readonly #started: number;\n readonly #ledger = new RealtimeLedger();\n readonly #events = pushQueue<RealtimeEvent>();\n readonly #turn: FishTurn = {\n dropAudio: false,\n discardFinals: 0,\n speaking: false,\n turnOpen: false,\n };\n #closed = false;\n #usage: Usage | undefined;\n\n constructor(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n room: FishRoom,\n now: () => number\n ) {\n this.model = model;\n this.#context = context;\n this.#room = room;\n this.#now = now;\n this.#started = now();\n }\n\n start(sessionId: string): void {\n this.#emit({ type: 'session_start', sessionId });\n }\n\n #assertOpen(): void {\n if (this.#closed) throw new VoiceRequestError('error', 'session is closed');\n }\n\n #send(event: JsonObject): void {\n void Promise.resolve(this.#room.publishData(JSON.stringify(event))).catch((error: unknown) => {\n this.#fail(error);\n });\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 const source = (async function* source() {\n yield chunk;\n })();\n void publishMicrophone(this.#room, { microphone: source }).catch((error: unknown) =>\n this.#fail(error)\n );\n }\n\n sendText(value: string): void {\n this.#assertOpen();\n this.#ledger.addText();\n this.#send(userMessage(value, true));\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 if (delivery === 'silent') this.#turn.discardFinals += 1;\n if (delivery === 'interrupt') this.#send({ type: 'user.interrupt' });\n this.#send(userMessage(value, delivery !== 'silent'));\n });\n }\n\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void {\n this.#assertOpen();\n const send = () => {\n this.#send(toolResult(callId, resultText(result), result.isError));\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.#send({ type: 'user.interrupt' });\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) this.#context.instructions = patch.instructions;\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.#room.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 handlers(): FishHandlers {\n return {\n data: (topic, json) => {\n if (topic !== 'agent-event' || !isRecord(json)) return;\n for (const event of ingestFishData(json)) this.#emit(event);\n },\n text: (identity, value, attributes) => {\n const packet: FishText = { identity, text: value, attributes };\n for (const event of ingestFishText(this.#turn, packet, this.#room.identity))\n this.#emit(event);\n },\n attribute: (key, value) => {\n if (key !== 'lk.agent.state') return;\n for (const event of ingestFishState(this.#turn, value)) this.#emit(event);\n },\n speakers: (identities) => {\n for (const event of ingestSpeakers(this.#turn, identities)) this.#emit(event);\n },\n audio: (chunk) => {\n for (const event of ingestFishAudio(this.#turn, chunk)) this.#emit(event);\n },\n fail: (error) => this.#fail(error),\n };\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\nexport async function connectFish(\n model: RealtimeModel<string>,\n context: RealtimeContext,\n options: RealtimeOptions | undefined,\n settings: FishOptions = {}\n): Promise<RealtimeSession> {\n if (options?.signal?.aborted === true) throw new VoiceRequestError('aborted', 'session aborted');\n const target = await joinTarget(model, context, options, settings);\n const sessionHolder: { session?: FishSession } = {};\n const room = await openRoom(settings, {\n data: (topic, json) => sessionHolder.session?.handlers().data(topic, json, undefined),\n text: (identity, value, attributes) =>\n sessionHolder.session?.handlers().text(identity, value, attributes),\n attribute: (key, value) => sessionHolder.session?.handlers().attribute(key, value),\n speakers: (identities) => sessionHolder.session?.handlers().speakers(identities),\n audio: (chunk) => sessionHolder.session?.handlers().audio(chunk),\n fail: (error) => sessionHolder.session?.handlers().fail(error),\n });\n const now = settings.now !== undefined ? settings.now : Date.now;\n const session = new FishSession(model, context, room, now);\n sessionHolder.session = session;\n await room.connect(target.url, target.token);\n if (room.identity.length === 0) room.identity = 'user';\n await publishMicrophone(room, settings);\n session.start(target.sessionId);\n return session;\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAA2B,SAAS,mBAAmB;AACvD,SAAS,yBAAyB;AAGlC,SAAS,iBAAiB;AAO1B,SAAS,gBAAgB,sBAAsB;AAC/C,SAAS,kBAAkB;AAC3B;AAAA,EAGE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAgC,sBAAsB;AACtD;AAAA,EAKE;AAAA,EACA;AAAA,OACK;AAcP,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,WAAW,QAAmC;AACrD,QAAM,QAAkB,CAAC;AACzB,aAAW,QAAQ,OAAO,SAAS;AACjC,QAAI,KAAK,SAAS,OAAQ,OAAM,KAAK,KAAK,IAAI;AAAA,EAChD;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,OAAO,SAA+C;AAC7D,SAAO,OAAO,SAAS,WAAW,YAAY,QAAQ,OAAO,SAAS;AACxE;AAEA,SAAS,UAAU,OAAsC;AACvD,QAAM,UAAU,MAAM;AACtB,MAAI,YAAY,OAAW,QAAO;AAClC,SAAO,QAAQ;AACjB;AAEO,SAAS,gBACd,OACA,SACiB;AACjB,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,cAAc,QAAQ;AAAA,IACtB,OAAO,QAAQ;AAAA,IACf,OAAO,QAAQ;AAAA,IACf,KAAK,UAAU,KAAK;AAAA,EACtB;AACF;AAEA,SAAS,YAAY,SAA8B;AACjD,SAAO,EAAE,SAAS,UAAU,IAAI,aAAa,EAAE;AACjD;AAEA,eAAe,YACb,OACA,SACA,SACA,UACsB;AACtB,MAAI,CAAC,OAAO,OAAO,GAAG;AACpB,UAAM,KAAK,SAAS;AACpB,QAAI,OAAO,UAAa,GAAG,WAAW,GAAG;AACvC,YAAM,IAAI,kBAAkB,SAAS,6BAA6B;AAAA,IACpE;AACA,WAAO,YAAY,EAAE;AAAA,EACvB;AACA,QAAM,YAA8B;AAAA,IAClC,GAAG;AAAA,IACH,SAAS,SAAS;AAAA,IAClB,OAAO,SAAS;AAAA,IAChB,KAAK,SAAS;AAAA,EAChB;AACA,SAAO,eAAe,OAAO,gBAAgB,OAAO,OAAO,GAAG,SAAS;AACzE;AAEA,SAAS,YACP,SACA,SACA,cACY;AACZ,QAAM,YAAwB,CAAC;AAC/B,MAAI,QAAQ,aAAa,SAAS,EAAG,WAAU,gBAAgB,QAAQ;AACvE,MAAI,QAAQ,MAAM,SAAS,EAAG,WAAU,WAAW,QAAQ;AAC3D,MAAI,iBAAiB,OAAW,WAAU,gBAAgB;AAC1D,QAAM,OAAmB,EAAE,UAAU,QAAQ;AAC7C,MAAI,OAAO,KAAK,SAAS,EAAE,SAAS,EAAG,MAAK,YAAY;AACxD,SAAO;AACT;AAEA,SAAS,YAAY,SAA8D;AACjF,QAAM,UAAkC,EAAE,gBAAgB,mBAAmB;AAC7E,QAAM,MAAM,SAAS;AACrB,MAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,EAAG,SAAQ,gBAAgB,UAAU,GAAG;AACpF,QAAM,QAAQ,SAAS;AACvB,MAAI,UAAU,OAAW,QAAO;AAChC,aAAW,CAACA,MAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,KAAM,QAAO,QAAQA,IAAG;AAAA,QACjC,SAAQA,IAAG,IAAI;AAAA,EACtB;AACA,SAAO;AACT;AAEA,eAAe,cACb,OACA,SACA,SACA,SACA,cAC4D;AAC5D,QAAM,YAAY,SAAS,SAAS,WAAW;AAC/C,QAAM,WAAW,MAAM,UAAU,GAAG,IAAI,IAAI,MAAM,OAAO,EAAE,MAAM,sBAAsB;AAAA,IACrF,QAAQ;AAAA,IACR,SAAS,YAAY,OAAO;AAAA,IAC5B,MAAM,KAAK,UAAU,YAAY,SAAS,SAAS,YAAY,CAAC;AAAA,IAChE,QAAQ,SAAS;AAAA,EACnB,CAAC;AACD,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,kBAAkB,SAAS,2BAA2B,SAAS,MAAM,GAAG;AACpF,QAAM,OAAgB,MAAM,SAAS,KAAK;AAC1C,MAAI,CAAC,SAAS,IAAI,EAAG,OAAM,IAAI,kBAAkB,SAAS,oCAAoC;AAC9F,MAAI,KAAK,cAAc,UAAW,OAAM,IAAI,kBAAkB,SAAS,uBAAuB;AAC9F,QAAM,MAAM,KAAK;AACjB,QAAM,QAAQ,KAAK;AACnB,QAAM,YAAY,KAAK;AACvB,MAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,GAAG;AAC/C,UAAM,IAAI,kBAAkB,SAAS,qBAAqB;AAAA,EAC5D;AACA,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,UAAM,IAAI,kBAAkB,SAAS,uBAAuB;AAAA,EAC9D;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,OAAO,cAAc,YAAY,UAAU,SAAS,IAAI,YAAY;AAAA,EACjF;AACF;AAEA,eAAe,WACb,OACA,SACA,SACA,UAC4D;AAC5D,QAAM,SAAS,SAAS;AACxB,MAAI,WAAW,UAAa,OAAO,MAAM,SAAS,GAAG;AACnD,UAAM,MAAM,OAAO;AACnB,QAAI,QAAQ,UAAa,IAAI,WAAW;AACtC,YAAM,IAAI,kBAAkB,SAAS,qBAAqB;AAC5D,WAAO,EAAE,KAAK,OAAO,OAAO,OAAO,WAAW,UAAU;AAAA,EAC1D;AACA,QAAM,QAAQ,MAAM,YAAY,OAAO,SAAS,SAAS,QAAQ;AACjE,SAAO,cAAc,OAAO,MAAM,SAAS,SAAS,SAAS,SAAS,YAAY;AACpF;AAEA,eAAe,SAAS,UAAuB,UAA2C;AACxF,MAAI,SAAS,SAAS,OAAW,QAAO,SAAS,KAAK,QAAQ;AAC9D,MAAI,SAAS,QAAQ,OAAW,QAAO,aAAa,SAAS,KAAK,QAAQ;AAC1E,SAAO,aAAa,MAAM,cAAc,SAAS,OAAO,GAAG,QAAQ;AACrE;AAEA,eAAe,kBAAkB,MAAgB,UAAsC;AACrF,QAAM,MAAM,SAAS;AACrB,MAAI,QAAQ,OAAW;AACvB,QAAM,OAAO,SAAS;AACtB,QAAM,OAAO,SAAS,SAAY,YAAY,KAAK;AACnD,QAAM,SAAS,SAAS,SAAY,MAAM,YAAY,MAAM,GAAG;AAC/D,QAAM,KAAK,WAAW,QAAQ,QAAQ,MAAM,KAAK,CAAC;AACpD;AAEA,MAAM,YAAuC;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU,IAAI,eAAe;AAAA,EAC7B,UAAU,UAAyB;AAAA,EACnC,QAAkB;AAAA,IACzB,WAAW;AAAA,IACX,eAAe;AAAA,IACf,UAAU;AAAA,IACV,UAAU;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EAEA,YACE,OACA,SACA,MACA,KACA;AACA,SAAK,QAAQ;AACb,SAAK,WAAW;AAChB,SAAK,QAAQ;AACb,SAAK,OAAO;AACZ,SAAK,WAAW,IAAI;AAAA,EACtB;AAAA,EAEA,MAAM,WAAyB;AAC7B,SAAK,MAAM,EAAE,MAAM,iBAAiB,UAAU,CAAC;AAAA,EACjD;AAAA,EAEA,cAAoB;AAClB,QAAI,KAAK,QAAS,OAAM,IAAI,kBAAkB,SAAS,mBAAmB;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAyB;AAC7B,SAAK,QAAQ,QAAQ,KAAK,MAAM,YAAY,KAAK,UAAU,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,UAAmB;AAC5F,WAAK,MAAM,KAAK;AAAA,IAClB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAA4B;AAChC,QAAI,KAAK,QAAS;AAClB,SAAK,QAAQ,QAAQ,KAAK;AAC1B,SAAK,QAAQ,KAAK,KAAK;AAAA,EACzB;AAAA,EAEA,YAAY,OAAyB;AACnC,SAAK,YAAY;AACjB,SAAK,QAAQ,SAAS,KAAK;AAC3B,UAAM,UAAU,gBAAgBC,UAAS;AACvC,YAAM;AAAA,IACR,GAAG;AACH,SAAK,kBAAkB,KAAK,OAAO,EAAE,YAAY,OAAO,CAAC,EAAE;AAAA,MAAM,CAAC,UAChE,KAAK,MAAM,KAAK;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,SAAS,OAAqB;AAC5B,SAAK,YAAY;AACjB,SAAK,QAAQ,QAAQ;AACrB,SAAK,MAAM,YAAY,OAAO,IAAI,CAAC;AAAA,EACrC;AAAA,EAEA,OAAO,OAAe,UAA0B;AAC9C,SAAK,YAAY;AACjB,SAAK,QAAQ,OAAO,KAAK,MAAM,UAAU,UAAU,MAAM;AACvD,WAAK,YAAY;AACjB,WAAK,QAAQ,QAAQ;AACrB,UAAI,aAAa,SAAU,MAAK,MAAM,iBAAiB;AACvD,UAAI,aAAa,YAAa,MAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACnE,WAAK,MAAM,YAAY,OAAO,aAAa,QAAQ,CAAC;AAAA,IACtD,CAAC;AAAA,EACH;AAAA,EAEA,QAAQ,QAAgB,QAA2B,UAA2B;AAC5E,SAAK,YAAY;AACjB,UAAM,OAAO,MAAM;AACjB,WAAK,MAAM,WAAW,QAAQ,WAAW,MAAM,GAAG,OAAO,OAAO,CAAC;AAAA,IACnE;AACA,QAAI,aAAa,QAAW;AAC1B,WAAK;AACL;AAAA,IACF;AACA,SAAK,QAAQ,OAAO,KAAK,MAAM,UAAU,UAAU,MAAM;AACvD,WAAK,YAAY;AACjB,WAAK;AAAA,IACP,CAAC;AAAA,EACH;AAAA,EAEA,UAAU,OAAmD;AAC3D,SAAK,YAAY;AACjB,mBAAe,KAAK,MAAM,YAAY,KAAK;AAC3C,SAAK,MAAM,YAAY;AACvB,SAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACrC,SAAK,QAAQ,OAAO;AAAA,EACtB;AAAA,EAEA,OAAO,OAAiF;AACtF,SAAK,YAAY;AACjB,QAAI,MAAM,UAAU,UAAa,KAAK,MAAM,mBAAmB,OAAO;AACpE,YAAM,IAAI,kBAAkB,SAAS,iCAAiC;AAAA,IACxE;AACA,QAAI,MAAM,UAAU,OAAW,MAAK,SAAS,QAAQ,MAAM;AAC3D,QAAI,MAAM,iBAAiB,OAAW,MAAK,SAAS,eAAe,MAAM;AACzE,QAAI,MAAM,UAAU,OAAW,MAAK,SAAS,QAAQ,MAAM;AAAA,EAC7D;AAAA,EAEA,MAAM,QAAmC;AACvC,QAAI,KAAK,WAAW,OAAW,QAAO,EAAE,OAAO,KAAK,OAAO;AAC3D,SAAK,QAAQ,OAAO;AACpB,SAAK,UAAU;AACf,SAAK,MAAM,MAAM;AACjB,SAAK,QAAQ,IAAI;AACjB,UAAM,eAAe,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,KAAK,QAAQ,IAAI;AAChE,SAAK,SAAS,WAAW,KAAK,MAAM,WAAW;AAAA,MAC7C;AAAA,MACA,eAAe,KAAK,QAAQ;AAAA,MAC5B,cAAc,KAAK,QAAQ;AAAA,IAC7B,CAAC;AACD,WAAO,EAAE,OAAO,KAAK,OAAO;AAAA,EAC9B;AAAA,EAEA,CAAC,OAAO,aAAa,IAAkC;AACrD,WAAO,KAAK,QAAQ,SAAS,OAAO,aAAa,EAAE;AAAA,EACrD;AAAA,EAEA,WAAyB;AACvB,WAAO;AAAA,MACL,MAAM,CAAC,OAAO,SAAS;AACrB,YAAI,UAAU,iBAAiB,CAAC,SAAS,IAAI,EAAG;AAChD,mBAAW,SAAS,eAAe,IAAI,EAAG,MAAK,MAAM,KAAK;AAAA,MAC5D;AAAA,MACA,MAAM,CAAC,UAAU,OAAO,eAAe;AACrC,cAAM,SAAmB,EAAE,UAAU,MAAM,OAAO,WAAW;AAC7D,mBAAW,SAAS,eAAe,KAAK,OAAO,QAAQ,KAAK,MAAM,QAAQ;AACxE,eAAK,MAAM,KAAK;AAAA,MACpB;AAAA,MACA,WAAW,CAAC,KAAK,UAAU;AACzB,YAAI,QAAQ,iBAAkB;AAC9B,mBAAW,SAAS,gBAAgB,KAAK,OAAO,KAAK,EAAG,MAAK,MAAM,KAAK;AAAA,MAC1E;AAAA,MACA,UAAU,CAAC,eAAe;AACxB,mBAAW,SAAS,eAAe,KAAK,OAAO,UAAU,EAAG,MAAK,MAAM,KAAK;AAAA,MAC9E;AAAA,MACA,OAAO,CAAC,UAAU;AAChB,mBAAW,SAAS,gBAAgB,KAAK,OAAO,KAAK,EAAG,MAAK,MAAM,KAAK;AAAA,MAC1E;AAAA,MACA,MAAM,CAAC,UAAU,KAAK,MAAM,KAAK;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,MAAM,OAAsB;AAC1B,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAK,MAAM,EAAE,MAAM,SAAS,OAAO,SAAS,OAAO,KAAK,CAAC;AAAA,EAC3D;AACF;AAEA,eAAsB,YACpB,OACA,SACA,SACA,WAAwB,CAAC,GACC;AAC1B,MAAI,SAAS,QAAQ,YAAY,KAAM,OAAM,IAAI,kBAAkB,WAAW,iBAAiB;AAC/F,QAAM,SAAS,MAAM,WAAW,OAAO,SAAS,SAAS,QAAQ;AACjE,QAAM,gBAA2C,CAAC;AAClD,QAAM,OAAO,MAAM,SAAS,UAAU;AAAA,IACpC,MAAM,CAAC,OAAO,SAAS,cAAc,SAAS,SAAS,EAAE,KAAK,OAAO,MAAM,MAAS;AAAA,IACpF,MAAM,CAAC,UAAU,OAAO,eACtB,cAAc,SAAS,SAAS,EAAE,KAAK,UAAU,OAAO,UAAU;AAAA,IACpE,WAAW,CAAC,KAAK,UAAU,cAAc,SAAS,SAAS,EAAE,UAAU,KAAK,KAAK;AAAA,IACjF,UAAU,CAAC,eAAe,cAAc,SAAS,SAAS,EAAE,SAAS,UAAU;AAAA,IAC/E,OAAO,CAAC,UAAU,cAAc,SAAS,SAAS,EAAE,MAAM,KAAK;AAAA,IAC/D,MAAM,CAAC,UAAU,cAAc,SAAS,SAAS,EAAE,KAAK,KAAK;AAAA,EAC/D,CAAC;AACD,QAAM,MAAM,SAAS,QAAQ,SAAY,SAAS,MAAM,KAAK;AAC7D,QAAM,UAAU,IAAI,YAAY,OAAO,SAAS,MAAM,GAAG;AACzD,gBAAc,UAAU;AACxB,QAAM,KAAK,QAAQ,OAAO,KAAK,OAAO,KAAK;AAC3C,MAAI,KAAK,SAAS,WAAW,EAAG,MAAK,WAAW;AAChD,QAAM,kBAAkB,MAAM,QAAQ;AACtC,UAAQ,MAAM,OAAO,SAAS;AAC9B,SAAO;AACT;",
|
|
6
|
+
"names": ["key", "source"]
|
|
7
|
+
}
|
package/dist/realtime.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/realtime.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n Message,\n ProviderRequestOptions,\n Tool,\n ToolCall,\n ToolResultMessage,\n Usage,\n} from '@earendil-works/pi-ai';\nimport {\n type AudioChunk,\n alignmentSeconds,\n type CharAlignment,\n chunkSeconds,\n type TextSpan,\n} from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { Delivery, DeliverySupport, RealtimeModel } from './models.ts';\nimport { priceAudio, type RealtimeAudioCost } from './usage.ts';\n\nexport interface RealtimeContext {\n instructions: string;\n tools: Tool[];\n voice: string;\n history?: Message[];\n}\n\nexport interface RealtimeToken {\n value: string;\n expiresAt: number;\n model: string;\n}\n\nexport interface RealtimeOptions extends ProviderRequestOptions {\n token?: RealtimeToken;\n transport?: 'websocket' | 'webrtc';\n gateway?: { url: string; token: string };\n resumeHandle?: string;\n}\n\nexport type RealtimeEvent =\n | { type: 'session_start'; sessionId: string }\n | { type: 'input_speech_start' }\n | { type: 'input_transcript'; itemId: string; text: string; final: boolean }\n | {\n type: 'output_audio';\n itemId: string;\n chunk: AudioChunk;\n span?: TextSpan;\n alignment?: CharAlignment;\n }\n | { type: 'output_transcript'; itemId: string; text: string; final: boolean }\n | { type: 'tool_call'; call: ToolCall; blocking: boolean }\n | { type: 'tool_call_cancelled'; callId: string }\n | { type: 'response_done'; itemId: string; interrupted: boolean; usage: Usage }\n | { type: 'go_away'; timeLeftMs: number }\n | { type: 'resumption'; handle: string }\n | { type: 'error'; error: string; fatal: boolean };\n\nexport interface RealtimeSession extends AsyncIterable<RealtimeEvent> {\n readonly model: RealtimeModel<string>;\n appendAudio(chunk: AudioChunk): void;\n appendVideoFrame?(jpeg: Uint8Array): void;\n sendText(text: string): void;\n inject(text: string, delivery: Delivery): void;\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void;\n interrupt(heard?: { itemId: string; heardMs: number }): void;\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void;\n close(): Promise<{ usage: Usage }>;\n}\n\nexport interface RealtimeApiProvider<TApi extends string = string> {\n api: TApi;\n connect: (\n model: RealtimeModel<TApi>,\n context: RealtimeContext,\n options?: RealtimeOptions\n ) => Promise<RealtimeSession>;\n}\n\nexport interface DeliveryPlan {\n delivery: Delivery;\n support: DeliverySupport;\n waitForIdle: boolean;\n}\n\nexport interface TruncationPlan {\n item?: { itemId: string; audioEndMs: number };\n}\n\nexport interface RealtimeSocket {\n url: string;\n protocols: string[];\n}\n\nexport function planDelivery(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery\n): DeliveryPlan {\n const support = delivery[requested];\n if (support !== 'native' && support !== 'approximate') {\n throw new VoiceRequestError('error', `unsupported delivery: ${requested}`);\n }\n return {\n delivery: requested,\n support,\n waitForIdle: requested === 'when_idle' && support === 'approximate',\n };\n}\n\nexport function planTruncation(\n truncation: RealtimeModel<string>['truncation'],\n heard?: { itemId: string; heardMs: number }\n): TruncationPlan {\n if (truncation === 'client' && heard !== undefined) {\n return { item: { itemId: heard.itemId, audioEndMs: heard.heardMs } };\n }\n return {};\n}\n\nexport function realtimeSocket(\n url: string,\n options?: Pick<RealtimeOptions, 'gateway'>,\n protocols: readonly string[] = []\n): RealtimeSocket {\n const gateway = options?.gateway;\n if (gateway === undefined) return { url, protocols: [...protocols] };\n return {\n url: gateway.url,\n protocols: [`cf-aig-authorization.${gateway.token}`, ...protocols],\n };\n}\n\nexport class RealtimeLedger {\n #inputSeconds = 0;\n #outputSeconds = 0;\n #textMessages = 0;\n #busy = false;\n #held: Array<() => void> = [];\n\n get inputSeconds(): number {\n return this.#inputSeconds;\n }\n\n get outputSeconds(): number {\n return this.#outputSeconds;\n }\n\n get textMessages(): number {\n return this.#textMessages;\n }\n\n get busy(): boolean {\n return this.#busy;\n }\n\n addInput(chunk: AudioChunk): void {\n this.#inputSeconds += chunkSeconds(chunk);\n }\n\n addOutput(chunk: AudioChunk, alignment?: CharAlignment): void {\n const seconds = chunkSeconds(chunk);\n this.#outputSeconds += seconds > 0 ? seconds : alignmentSeconds(alignment);\n }\n\n addText(): void {\n this.#textMessages += 1;\n }\n\n observe(event: RealtimeEvent): void {\n if (event.type === 'output_audio') this.addOutput(event.chunk, event.alignment);\n if (event.type === 'output_audio' || event.type === 'output_transcript') this.#busy = true;\n if (event.type === 'response_done') this.#release();\n }\n\n submit(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery,\n run: () => void\n ): DeliveryPlan {\n const plan = planDelivery(delivery, requested);\n if (plan.waitForIdle && this.#busy) this.#held.push(run);\n else run();\n return plan;\n }\n\n settle(): void {\n this.#release();\n }\n\n usage(rates: RealtimeAudioCost): Usage {\n return priceAudio(rates, {\n inputSeconds: this.#inputSeconds,\n outputSeconds: this.#outputSeconds,\n textMessages: this.#textMessages,\n });\n }\n\n #release(): void {\n this.#busy = false;\n const held = this.#held;\n this.#held = [];\n for (const run of held) run();\n }\n}\n"],
|
|
5
|
-
"mappings": "AAQA;AAAA,EAEE;AAAA,EAEA;AAAA,OAEK;AACP,SAAS,yBAAyB;AAElC,SAAS,kBAA0C;
|
|
4
|
+
"sourcesContent": ["import type {\n Message,\n ProviderRequestOptions,\n Tool,\n ToolCall,\n ToolResultMessage,\n Usage,\n} from '@earendil-works/pi-ai';\nimport {\n type AudioChunk,\n alignmentSeconds,\n type CharAlignment,\n chunkSeconds,\n type TextSpan,\n} from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { Delivery, DeliverySupport, RealtimeModel } from './models.ts';\nimport { priceAudio, type RealtimeAudioCost } from './usage.ts';\n\nexport interface RealtimeContext {\n instructions: string;\n tools: Tool[];\n voice: string;\n history?: Message[];\n}\n\nexport interface RealtimeToken {\n value: string;\n expiresAt: number;\n model: string;\n url?: string;\n}\n\nexport interface RealtimeOptions extends ProviderRequestOptions {\n token?: RealtimeToken;\n transport?: 'websocket' | 'webrtc';\n gateway?: { url: string; token: string };\n resumeHandle?: string;\n}\n\nexport type RealtimeEvent =\n | { type: 'session_start'; sessionId: string }\n | { type: 'input_speech_start' }\n | { type: 'input_transcript'; itemId: string; text: string; final: boolean }\n | {\n type: 'output_audio';\n itemId: string;\n chunk: AudioChunk;\n span?: TextSpan;\n alignment?: CharAlignment;\n }\n | { type: 'output_transcript'; itemId: string; text: string; final: boolean }\n | { type: 'tool_call'; call: ToolCall; blocking: boolean }\n | { type: 'tool_call_cancelled'; callId: string }\n | { type: 'response_done'; itemId: string; interrupted: boolean; usage: Usage }\n | { type: 'go_away'; timeLeftMs: number }\n | { type: 'resumption'; handle: string }\n | { type: 'error'; error: string; fatal: boolean };\n\nexport interface RealtimeSession extends AsyncIterable<RealtimeEvent> {\n readonly model: RealtimeModel<string>;\n appendAudio(chunk: AudioChunk): void;\n appendVideoFrame?(jpeg: Uint8Array): void;\n sendText(text: string): void;\n inject(text: string, delivery: Delivery): void;\n respond(callId: string, result: ToolResultMessage, delivery?: Delivery): void;\n interrupt(heard?: { itemId: string; heardMs: number }): void;\n update(patch: Partial<Pick<RealtimeContext, 'instructions' | 'tools' | 'voice'>>): void;\n close(): Promise<{ usage: Usage }>;\n}\n\nexport interface RealtimeApiProvider<TApi extends string = string> {\n api: TApi;\n connect: (\n model: RealtimeModel<TApi>,\n context: RealtimeContext,\n options?: RealtimeOptions\n ) => Promise<RealtimeSession>;\n}\n\nexport interface DeliveryPlan {\n delivery: Delivery;\n support: DeliverySupport;\n waitForIdle: boolean;\n}\n\nexport interface TruncationPlan {\n item?: { itemId: string; audioEndMs: number };\n}\n\nexport interface RealtimeSocket {\n url: string;\n protocols: string[];\n}\n\nexport function planDelivery(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery\n): DeliveryPlan {\n const support = delivery[requested];\n if (support !== 'native' && support !== 'approximate') {\n throw new VoiceRequestError('error', `unsupported delivery: ${requested}`);\n }\n return {\n delivery: requested,\n support,\n waitForIdle: requested === 'when_idle' && support === 'approximate',\n };\n}\n\nexport function planTruncation(\n truncation: RealtimeModel<string>['truncation'],\n heard?: { itemId: string; heardMs: number }\n): TruncationPlan {\n if (truncation === 'client' && heard !== undefined) {\n return { item: { itemId: heard.itemId, audioEndMs: heard.heardMs } };\n }\n return {};\n}\n\nexport function realtimeSocket(\n url: string,\n options?: Pick<RealtimeOptions, 'gateway'>,\n protocols: readonly string[] = []\n): RealtimeSocket {\n const gateway = options?.gateway;\n if (gateway === undefined) return { url, protocols: [...protocols] };\n return {\n url: gateway.url,\n protocols: [`cf-aig-authorization.${gateway.token}`, ...protocols],\n };\n}\n\nexport class RealtimeLedger {\n #inputSeconds = 0;\n #outputSeconds = 0;\n #textMessages = 0;\n #busy = false;\n #held: Array<() => void> = [];\n\n get inputSeconds(): number {\n return this.#inputSeconds;\n }\n\n get outputSeconds(): number {\n return this.#outputSeconds;\n }\n\n get textMessages(): number {\n return this.#textMessages;\n }\n\n get busy(): boolean {\n return this.#busy;\n }\n\n addInput(chunk: AudioChunk): void {\n this.#inputSeconds += chunkSeconds(chunk);\n }\n\n addOutput(chunk: AudioChunk, alignment?: CharAlignment): void {\n const seconds = chunkSeconds(chunk);\n this.#outputSeconds += seconds > 0 ? seconds : alignmentSeconds(alignment);\n }\n\n addText(): void {\n this.#textMessages += 1;\n }\n\n observe(event: RealtimeEvent): void {\n if (event.type === 'output_audio') this.addOutput(event.chunk, event.alignment);\n if (event.type === 'output_audio' || event.type === 'output_transcript') this.#busy = true;\n if (event.type === 'response_done') this.#release();\n }\n\n submit(\n delivery: Record<Delivery, DeliverySupport>,\n requested: Delivery,\n run: () => void\n ): DeliveryPlan {\n const plan = planDelivery(delivery, requested);\n if (plan.waitForIdle && this.#busy) this.#held.push(run);\n else run();\n return plan;\n }\n\n settle(): void {\n this.#release();\n }\n\n usage(rates: RealtimeAudioCost): Usage {\n return priceAudio(rates, {\n inputSeconds: this.#inputSeconds,\n outputSeconds: this.#outputSeconds,\n textMessages: this.#textMessages,\n });\n }\n\n #release(): void {\n this.#busy = false;\n const held = this.#held;\n this.#held = [];\n for (const run of held) run();\n }\n}\n"],
|
|
5
|
+
"mappings": "AAQA;AAAA,EAEE;AAAA,EAEA;AAAA,OAEK;AACP,SAAS,yBAAyB;AAElC,SAAS,kBAA0C;AA8E5C,SAAS,aACd,UACA,WACc;AACd,QAAM,UAAU,SAAS,SAAS;AAClC,MAAI,YAAY,YAAY,YAAY,eAAe;AACrD,UAAM,IAAI,kBAAkB,SAAS,yBAAyB,SAAS,EAAE;AAAA,EAC3E;AACA,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA,aAAa,cAAc,eAAe,YAAY;AAAA,EACxD;AACF;AAEO,SAAS,eACd,YACA,OACgB;AAChB,MAAI,eAAe,YAAY,UAAU,QAAW;AAClD,WAAO,EAAE,MAAM,EAAE,QAAQ,MAAM,QAAQ,YAAY,MAAM,QAAQ,EAAE;AAAA,EACrE;AACA,SAAO,CAAC;AACV;AAEO,SAAS,eACd,KACA,SACA,YAA+B,CAAC,GAChB;AAChB,QAAM,UAAU,SAAS;AACzB,MAAI,YAAY,OAAW,QAAO,EAAE,KAAK,WAAW,CAAC,GAAG,SAAS,EAAE;AACnE,SAAO;AAAA,IACL,KAAK,QAAQ;AAAA,IACb,WAAW,CAAC,wBAAwB,QAAQ,KAAK,IAAI,GAAG,SAAS;AAAA,EACnE;AACF;AAEO,MAAM,eAAe;AAAA,EAC1B,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,QAA2B,CAAC;AAAA,EAE5B,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,gBAAwB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,eAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,OAAgB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS,OAAyB;AAChC,SAAK,iBAAiB,aAAa,KAAK;AAAA,EAC1C;AAAA,EAEA,UAAU,OAAmB,WAAiC;AAC5D,UAAM,UAAU,aAAa,KAAK;AAClC,SAAK,kBAAkB,UAAU,IAAI,UAAU,iBAAiB,SAAS;AAAA,EAC3E;AAAA,EAEA,UAAgB;AACd,SAAK,iBAAiB;AAAA,EACxB;AAAA,EAEA,QAAQ,OAA4B;AAClC,QAAI,MAAM,SAAS,eAAgB,MAAK,UAAU,MAAM,OAAO,MAAM,SAAS;AAC9E,QAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,oBAAqB,MAAK,QAAQ;AACtF,QAAI,MAAM,SAAS,gBAAiB,MAAK,SAAS;AAAA,EACpD;AAAA,EAEA,OACE,UACA,WACA,KACc;AACd,UAAM,OAAO,aAAa,UAAU,SAAS;AAC7C,QAAI,KAAK,eAAe,KAAK,MAAO,MAAK,MAAM,KAAK,GAAG;AAAA,QAClD,KAAI;AACT,WAAO;AAAA,EACT;AAAA,EAEA,SAAe;AACb,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,OAAiC;AACrC,WAAO,WAAW,OAAO;AAAA,MACvB,cAAc,KAAK;AAAA,MACnB,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,IACrB,CAAC;AAAA,EACH;AAAA,EAEA,WAAiB;AACf,SAAK,QAAQ;AACb,UAAM,OAAO,KAAK;AAClB,SAAK,QAAQ,CAAC;AACd,eAAW,OAAO,KAAM,KAAI;AAAA,EAC9B;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/soundscape.js
CHANGED
|
@@ -14,6 +14,9 @@ function followBed(bed, soundscape, wait) {
|
|
|
14
14
|
const stop = () => {
|
|
15
15
|
generation += 1;
|
|
16
16
|
clearTimer();
|
|
17
|
+
if (playing === void 0) return;
|
|
18
|
+
playing = void 0;
|
|
19
|
+
bed.stop(fadeOf(soundscape));
|
|
17
20
|
};
|
|
18
21
|
const observe = (phase) => {
|
|
19
22
|
const cues = listCues(soundscape.cue(phase, previous));
|
|
@@ -69,10 +72,19 @@ function firstLoop(cues) {
|
|
|
69
72
|
}
|
|
70
73
|
return void 0;
|
|
71
74
|
}
|
|
75
|
+
const sourceIds = /* @__PURE__ */ new WeakMap();
|
|
76
|
+
let sourceSerial = 0;
|
|
77
|
+
function sourceIdentity(value) {
|
|
78
|
+
const existing = sourceIds.get(value);
|
|
79
|
+
if (existing !== void 0) return existing;
|
|
80
|
+
sourceSerial += 1;
|
|
81
|
+
sourceIds.set(value, sourceSerial);
|
|
82
|
+
return sourceSerial;
|
|
83
|
+
}
|
|
72
84
|
function sourceKey(source) {
|
|
73
85
|
if ("url" in source) return `url:${source.url}`;
|
|
74
|
-
if ("buffer" in source) return `buffer:${source.buffer
|
|
75
|
-
return
|
|
86
|
+
if ("buffer" in source) return `buffer:${sourceIdentity(source.buffer)}`;
|
|
87
|
+
return `synth:${sourceIdentity(source.synth)}`;
|
|
76
88
|
}
|
|
77
89
|
function fadeOf(soundscape) {
|
|
78
90
|
const fade = soundscape.fadeMs;
|
package/dist/soundscape.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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
|
|
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,
|
|
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 if (playing === undefined) return;\n playing = undefined;\n bed.stop(fadeOf(soundscape));\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\nconst sourceIds = new WeakMap<object, number>();\nlet sourceSerial = 0;\n\nfunction sourceIdentity(value: object): number {\n const existing = sourceIds.get(value);\n if (existing !== undefined) return existing;\n sourceSerial += 1;\n sourceIds.set(value, sourceSerial);\n return sourceSerial;\n}\n\nfunction sourceKey(source: CueSource): string {\n if ('url' in source) return `url:${source.url}`;\n if ('buffer' in source) return `buffer:${sourceIdentity(source.buffer)}`;\n return `synth:${sourceIdentity(source.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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/tokens.js
CHANGED
|
@@ -159,6 +159,7 @@ function mintCall(model, constraints, options, key, now) {
|
|
|
159
159
|
headers: mergedHeaders(options, { ...jsonHeaders, Authorization: `Bearer ${key}` }),
|
|
160
160
|
body: JSON.stringify(fishBody(constraints)),
|
|
161
161
|
field: "token",
|
|
162
|
+
urlField: "livekit_url",
|
|
162
163
|
expires: fromField("expires_at"),
|
|
163
164
|
fallbackExpires: now
|
|
164
165
|
};
|
|
@@ -174,6 +175,12 @@ function readValue(json, field) {
|
|
|
174
175
|
if (typeof value === "string" && value.length > 0) return value;
|
|
175
176
|
throw new VoiceRequestError("error", "mint failed (empty token)");
|
|
176
177
|
}
|
|
178
|
+
function readUrl(json, field) {
|
|
179
|
+
if (field === void 0) return void 0;
|
|
180
|
+
const value = json[field];
|
|
181
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
182
|
+
throw new VoiceRequestError("error", "mint failed (missing livekit url)");
|
|
183
|
+
}
|
|
177
184
|
async function mintRealtimeToken(model, constraints, options) {
|
|
178
185
|
const settings = options ?? {};
|
|
179
186
|
const key = apiKey(settings);
|
|
@@ -188,11 +195,14 @@ async function mintRealtimeToken(model, constraints, options) {
|
|
|
188
195
|
});
|
|
189
196
|
if (!response.ok) throw new VoiceRequestError("error", `mint failed (${response.status})`);
|
|
190
197
|
const json = await response.json();
|
|
191
|
-
|
|
198
|
+
const token = {
|
|
192
199
|
value: readValue(json, call.field),
|
|
193
200
|
expiresAt: call.expires(json, call.fallbackExpires),
|
|
194
201
|
model: model.id
|
|
195
202
|
};
|
|
203
|
+
const url = readUrl(json, call.urlField);
|
|
204
|
+
if (url !== void 0) token.url = url;
|
|
205
|
+
return token;
|
|
196
206
|
}
|
|
197
207
|
export {
|
|
198
208
|
mintRealtimeToken
|