@ai-ecoverse/pi-audio 1.25.0 → 1.27.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/brain/call.js +36 -0
- package/dist/brain/call.js.map +7 -0
- package/dist/brain/hash.js +12 -0
- package/dist/brain/hash.js.map +7 -0
- package/dist/brain/index.js +38 -0
- package/dist/brain/index.js.map +7 -0
- package/dist/brain/options.js +1 -0
- package/dist/brain/options.js.map +7 -0
- package/dist/brain/parse.js +102 -0
- package/dist/brain/parse.js.map +7 -0
- package/dist/brain/sse.js +25 -0
- package/dist/brain/sse.js.map +7 -0
- package/dist/brain/turn.js +135 -0
- package/dist/brain/turn.js.map +7 -0
- package/dist/echo.js +46 -2
- package/dist/echo.js.map +2 -2
- package/dist/index.js.map +2 -2
- package/dist/types/brain/call.d.ts +7 -0
- package/dist/types/brain/hash.d.ts +1 -0
- package/dist/types/brain/index.d.ts +3 -0
- package/dist/types/brain/options.d.ts +12 -0
- package/dist/types/brain/parse.d.ts +22 -0
- package/dist/types/brain/sse.d.ts +3 -0
- package/dist/types/brain/turn.d.ts +5 -0
- package/dist/types/echo.d.ts +6 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/webrtc-aec/index.d.ts +19 -0
- package/dist/webrtc-aec/index.js +80 -0
- package/dist/webrtc-aec/index.js.map +7 -0
- package/package.json +14 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
35
35
|
|
|
36
36
|
`@ai-ecoverse/pi-audio/elevenlabs` registers ElevenLabs speech adapters when `registerElevenLabs()` is called. `scribe_v2` posts multipart audio to `/speech-to-text`. `scribe_v2_realtime` streams base64 audio on `wss://…/speech-to-text/realtime` and commits turns with `turn_prediction` (`turnDetection: 'semantic'`). `eleven_flash_v2_5` and `eleven_multilingual_v2` post JSON to `/text-to-speech/{voice}/with-timestamps` for a string and stream text on `stream-input` for an async iterable, with character alignment. `eleven_v3` and `eleven_v4` stay on the HTTP path (`textStreaming: false`). Scribe prices are $0.22 per hour for batch and $0.39 per hour for realtime.
|
|
37
37
|
|
|
38
|
-
`calibrateEcho(out, mic, probe, durationMs, signal)` plays a probe and aligns the mic with `AudioOut.reference`. `delayMs` is the lag. `cancellationDb` is how far the leaked copy sits below the reference, and `residualDb` is that leak. A quiet leak recommends `interrupt`, a moderate one `onWords`, and a loud one `headphones`. `chooseCapture` keeps `none` and `wasm`, and moves `browser` to `wasm` when cancellation is under 12 dB. `micOpen('browser')` asks for `echoCancellation` and `playback: 'rtc-loopback'`. `wasm` and `none` turn browser cancellation off and use `webaudio`. `assertNodeEcho` rejects `browser` on Node. `openEchoMic` leaves `browser` and `none` audio as captured. `wasm` runs an injected `EchoCanceller` against `reference`.
|
|
38
|
+
`calibrateEcho(out, mic, probe, durationMs, signal)` plays a probe and aligns the mic with `AudioOut.reference`. `delayMs` is the lag. `cancellationDb` is how far the leaked copy sits below the reference, and `residualDb` is that leak. A quiet leak recommends `interrupt`, a moderate one `onWords`, and a loud one `headphones`. `chooseCapture` keeps `none` and `wasm`, and moves `browser` to `wasm` when cancellation is under 12 dB. `micOpen('browser')` asks for `echoCancellation` and `playback: 'rtc-loopback'`. `wasm` and `none` turn browser cancellation off and use `webaudio`. `assertNodeEcho` rejects `browser` on Node. `openEchoMic` leaves `browser` and `none` audio as captured. `wasm` runs an injected `EchoCanceller` against `reference`. `@ai-ecoverse/pi-audio/webrtc-aec` adapts `@ai-ecoverse/wasm-webrtc-audio-processing` (optional peer, WebRTC AEC3) into that canceller. `createWebRtcEcho({ wasm, sampleRate })` instantiates the module the caller compiled, which is what an AudioWorklet can do. `createNodeWebRtcEcho({ sampleRate })` loads the package's wasm on Node. Mic and reference chunks are converted to mono float at 16 kHz or 48 kHz, and any non-finite sample is replaced with 0 before `cancel`, including after the canceller's own NaN fix. `stats()` supplies `delayMs` and echo return loss enhancement. `calibrateEcho` takes that canceller as its last argument: it cancels the captured pair, keeps the measured residual, and uses those stats for `delayMs` and `cancellationDb` when they are finite. `voiceTurns` passes `options.echo` through `openEchoMic` before `listen`.
|
|
39
39
|
|
|
40
40
|
`openNodeAudio({ sink: 'none' })` builds that mix on Node. `node-web-audio-api` (optional peer, BSD-3) is the `AudioContext`, created with `sinkId: { type: 'none' }` when the machine has no output device. `web-audio-api` (the audiojs package, MIT) is the pure-JS fallback: it accepts the same sink value but does not render unless a speaker or a stream is attached, so a silent sink is a discarded stream. Hosts pass the microphone as an `AudioSource`. The package does not open `node-web-audio-api`'s mic, and Node echo mode stays `wasm` or `none` with `playback: 'webaudio'`. Finished worklets post `pi-audio-release` and `bindWorkletNode` disconnects them. Returning `false` from `process` does not stop a processor in node-web-audio-api (issue 202). `openMixer(context)` is the same graph on a context the host already opened.
|
|
41
41
|
|
|
@@ -64,6 +64,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
64
64
|
|
|
65
65
|
`@ai-ecoverse/pi-audio/fish-agents` registers `fish-agent` (`transports: ["webrtc"]`, `delivery.silent: "approximate"`, `delivery.interrupt: "native"`). `provisionAgent` creates client tools, writes the draft, and publishes it. `fishCustomLlm` sends `llm.custom.base_url`, `model`, and a raw `api_key`, and rejects `openai-responses`. A `RealtimeToken` with `url` is the LiveKit room from `mintRealtimeToken`; connect does not open a second session. Without a token, connect creates the session and fails on an unknown `transport`. Silent `inject` sends `user.message` with `audio: false` and drops that reply's transcript. The microphone is published after `openEchoMic`. The catalog `audioCost` is zero because Fish has not published a per-minute session rate. The example voice id is the one in Fish's quickstart.
|
|
66
66
|
|
|
67
|
+
`@ai-ecoverse/pi-audio/brain` exports `chatCompletionsBrain`, a fetch handler for a pi agent behind `CustomLlm.baseUrl`. The session key is Fish `session_id`, or `session_id` inside `fishaudio_extra_body`, `llm_extra_body`, `elevenlabs_extra_body`, or `custom_llm_extra_body` (a string extra body is the key). A hash of the streamed reply prompts only the new user text when it matches. The same user text with a different hash replays that stream. A new user text with a different hash is prompted as `The caller heard:` plus the vendor's copy. Final-answer text is streamed. Request tools are terminal `tool_calls` when `vendorTools` is `terminal`. `filler` is written when no text arrives within `firstTokenMs`. A turn still open at `hardDeadlineMs` ends as an error. Fish recommends a first token under 800 ms and caps a generation at 10 s.
|
|
68
|
+
|
|
67
69
|
`talkerTools` is `ask_agent` and `read`. pi-audio defines `askAgent` (a pi `Tool` whose parameters are `request` and optional `context`) and the `Thinker` interface. The host implements `Thinker` and supplies `read`. `bindThinker` runs direct tools with `directTimeoutMs` and truncates their text to `directMaxChars`. On a blocking `ask_agent` it answers with `ack` immediately and injects the result later. On an async call it `respond`s with the result and `options.result`. Progress is injected with `options.progress`. `tool_call_cancelled` aborts the thinker and any direct tool. A blocking failure is injected after the acknowledgement, with a `thinker_result` caption. `Thinker.ask` receives the optional `context` string. Closing the session aborts work that is still running. The binding also yields `thinker_progress` / `thinker_result` captions and `thinking` / `tool` / `idle` phases.
|
|
68
70
|
|
|
69
71
|
## Development
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function contentBlocks(event) {
|
|
5
|
+
const direct = event.message?.content;
|
|
6
|
+
if (Array.isArray(direct)) return direct;
|
|
7
|
+
const model = event.entry?.model;
|
|
8
|
+
if (!Array.isArray(model) || model.length === 0) return [];
|
|
9
|
+
const first = model[0];
|
|
10
|
+
if (!isRecord(first) || !Array.isArray(first.content)) return [];
|
|
11
|
+
return first.content;
|
|
12
|
+
}
|
|
13
|
+
function callId(id, name) {
|
|
14
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
15
|
+
return name;
|
|
16
|
+
}
|
|
17
|
+
function vendorCall(event, tools) {
|
|
18
|
+
if (event.type === "tool_execution_start" && event.toolName && tools.has(event.toolName)) {
|
|
19
|
+
return {
|
|
20
|
+
id: callId(event.toolCallId, event.toolName),
|
|
21
|
+
name: event.toolName,
|
|
22
|
+
arguments: JSON.stringify(event.args ?? {})
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
for (const block of contentBlocks(event)) {
|
|
26
|
+
if (!isRecord(block) || block.type !== "toolCall" || typeof block.name !== "string") continue;
|
|
27
|
+
if (!tools.has(block.name)) continue;
|
|
28
|
+
const args = isRecord(block.arguments) ? block.arguments : {};
|
|
29
|
+
return { id: callId(block.id, block.name), name: block.name, arguments: JSON.stringify(args) };
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
vendorCall
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=call.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/call.ts"],
|
|
4
|
+
"sourcesContent": ["import type { VoiceAgentEvent } from '../spoken.ts';\n\nexport interface VendorCall {\n id: string;\n name: string;\n arguments: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction contentBlocks(event: VoiceAgentEvent): readonly unknown[] {\n const direct = event.message?.content;\n if (Array.isArray(direct)) return direct;\n const model = event.entry?.model;\n if (!Array.isArray(model) || model.length === 0) return [];\n const first = model[0];\n if (!isRecord(first) || !Array.isArray(first.content)) return [];\n return first.content;\n}\n\nfunction callId(id: unknown, name: string): string {\n if (typeof id === 'string' && id.length > 0) return id;\n return name;\n}\n\nexport function vendorCall(event: VoiceAgentEvent, tools: Set<string>): VendorCall | undefined {\n if (event.type === 'tool_execution_start' && event.toolName && tools.has(event.toolName)) {\n return {\n id: callId(event.toolCallId, event.toolName),\n name: event.toolName,\n arguments: JSON.stringify(event.args ?? {}),\n };\n }\n for (const block of contentBlocks(event)) {\n if (!isRecord(block) || block.type !== 'toolCall' || typeof block.name !== 'string') continue;\n if (!tools.has(block.name)) continue;\n const args = isRecord(block.arguments) ? block.arguments : {};\n return { id: callId(block.id, block.name), name: block.name, arguments: JSON.stringify(args) };\n }\n return undefined;\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,cAAc,OAA4C;AACjE,QAAM,SAAS,MAAM,SAAS;AAC9B,MAAI,MAAM,QAAQ,MAAM,EAAG,QAAO;AAClC,QAAM,QAAQ,MAAM,OAAO;AAC3B,MAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AACzD,QAAM,QAAQ,MAAM,CAAC;AACrB,MAAI,CAAC,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,MAAM,OAAO,EAAG,QAAO,CAAC;AAC/D,SAAO,MAAM;AACf;AAEA,SAAS,OAAO,IAAa,MAAsB;AACjD,MAAI,OAAO,OAAO,YAAY,GAAG,SAAS,EAAG,QAAO;AACpD,SAAO;AACT;AAEO,SAAS,WAAW,OAAwB,OAA4C;AAC7F,MAAI,MAAM,SAAS,0BAA0B,MAAM,YAAY,MAAM,IAAI,MAAM,QAAQ,GAAG;AACxF,WAAO;AAAA,MACL,IAAI,OAAO,MAAM,YAAY,MAAM,QAAQ;AAAA,MAC3C,MAAM,MAAM;AAAA,MACZ,WAAW,KAAK,UAAU,MAAM,QAAQ,CAAC,CAAC;AAAA,IAC5C;AAAA,EACF;AACA,aAAW,SAAS,cAAc,KAAK,GAAG;AACxC,QAAI,CAAC,SAAS,KAAK,KAAK,MAAM,SAAS,cAAc,OAAO,MAAM,SAAS,SAAU;AACrF,QAAI,CAAC,MAAM,IAAI,MAAM,IAAI,EAAG;AAC5B,UAAM,OAAO,SAAS,MAAM,SAAS,IAAI,MAAM,YAAY,CAAC;AAC5D,WAAO,EAAE,IAAI,OAAO,MAAM,IAAI,MAAM,IAAI,GAAG,MAAM,MAAM,MAAM,WAAW,KAAK,UAAU,IAAI,EAAE;AAAA,EAC/F;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function replyHash(text) {
|
|
2
|
+
let hash = 2166136261;
|
|
3
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
4
|
+
hash ^= text.charCodeAt(index);
|
|
5
|
+
hash = Math.imul(hash, 16777619);
|
|
6
|
+
}
|
|
7
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
replyHash
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=hash.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/hash.ts"],
|
|
4
|
+
"sourcesContent": ["export function replyHash(text: string): string {\n let hash = 0x811c9dc5;\n for (let index = 0; index < text.length; index += 1) {\n hash ^= text.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n"],
|
|
5
|
+
"mappings": "AAAO,SAAS,UAAU,MAAsB;AAC9C,MAAI,OAAO;AACX,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;AACnD,YAAQ,KAAK,WAAW,KAAK;AAC7B,WAAO,KAAK,KAAK,MAAM,QAAU;AAAA,EACnC;AACA,UAAQ,SAAS,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAClD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parseTurn, turnPlan } from "./parse.js";
|
|
2
|
+
import { runTurn } from "./turn.js";
|
|
3
|
+
function jsonError(message) {
|
|
4
|
+
return Response.json({ error: { message } }, { status: 400 });
|
|
5
|
+
}
|
|
6
|
+
async function readBody(request) {
|
|
7
|
+
try {
|
|
8
|
+
return await request.json();
|
|
9
|
+
} catch {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function chatCompletionsBrain(options) {
|
|
14
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
15
|
+
return async (request) => {
|
|
16
|
+
const body = await readBody(request);
|
|
17
|
+
if (body === void 0) return jsonError("invalid chat completion");
|
|
18
|
+
const parsed = parseTurn(body);
|
|
19
|
+
if (typeof parsed === "string") return jsonError(parsed);
|
|
20
|
+
const plan = turnPlan(sessions.get(parsed.sessionKey), parsed);
|
|
21
|
+
const stream = new ReadableStream({
|
|
22
|
+
async start(controller) {
|
|
23
|
+
try {
|
|
24
|
+
await runTurn(options, sessions, parsed, plan, request, controller);
|
|
25
|
+
} finally {
|
|
26
|
+
controller.close();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return new Response(stream, {
|
|
31
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-cache" }
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
chatCompletionsBrain
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { BrainOptions } from './options.ts';\nimport type { Memory } from './parse.ts';\nimport { parseTurn, turnPlan } from './parse.ts';\nimport { runTurn } from './turn.ts';\n\nexport type { BrainBudget, BrainOptions } from './options.ts';\n\nfunction jsonError(message: string): Response {\n return Response.json({ error: { message } }, { status: 400 });\n}\n\nasync function readBody(request: Request): Promise<unknown> {\n try {\n return await request.json();\n } catch {\n return undefined;\n }\n}\n\nexport function chatCompletionsBrain(\n options: BrainOptions\n): (request: Request) => Promise<Response> {\n const sessions = new Map<string, Memory>();\n return async (request) => {\n const body = await readBody(request);\n if (body === undefined) return jsonError('invalid chat completion');\n const parsed = parseTurn(body);\n if (typeof parsed === 'string') return jsonError(parsed);\n const plan = turnPlan(sessions.get(parsed.sessionKey), parsed);\n const stream = new ReadableStream<Uint8Array>({\n async start(controller) {\n try {\n await runTurn(options, sessions, parsed, plan, request, controller);\n } finally {\n controller.close();\n }\n },\n });\n return new Response(stream, {\n headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' },\n });\n };\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,WAAW,gBAAgB;AACpC,SAAS,eAAe;AAIxB,SAAS,UAAU,SAA2B;AAC5C,SAAO,SAAS,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9D;AAEA,eAAe,SAAS,SAAoC;AAC1D,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,qBACd,SACyC;AACzC,QAAM,WAAW,oBAAI,IAAoB;AACzC,SAAO,OAAO,YAAY;AACxB,UAAM,OAAO,MAAM,SAAS,OAAO;AACnC,QAAI,SAAS,OAAW,QAAO,UAAU,yBAAyB;AAClE,UAAM,SAAS,UAAU,IAAI;AAC7B,QAAI,OAAO,WAAW,SAAU,QAAO,UAAU,MAAM;AACvD,UAAM,OAAO,SAAS,SAAS,IAAI,OAAO,UAAU,GAAG,MAAM;AAC7D,UAAM,SAAS,IAAI,eAA2B;AAAA,MAC5C,MAAM,MAAM,YAAY;AACtB,YAAI;AACF,gBAAM,QAAQ,SAAS,UAAU,QAAQ,MAAM,SAAS,UAAU;AAAA,QACpE,UAAE;AACA,qBAAW,MAAM;AAAA,QACnB;AAAA,MACF;AAAA,IACF,CAAC;AACD,WAAO,IAAI,SAAS,QAAQ;AAAA,MAC1B,SAAS,EAAE,gBAAgB,qBAAqB,iBAAiB,WAAW;AAAA,IAC9E,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { replyHash } from "./hash.js";
|
|
2
|
+
const extraNames = [
|
|
3
|
+
"fishaudio_extra_body",
|
|
4
|
+
"llm_extra_body",
|
|
5
|
+
"elevenlabs_extra_body",
|
|
6
|
+
"custom_llm_extra_body"
|
|
7
|
+
];
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function textOf(content) {
|
|
12
|
+
if (typeof content === "string") return content;
|
|
13
|
+
if (!Array.isArray(content)) return "";
|
|
14
|
+
let text = "";
|
|
15
|
+
for (const part of content) {
|
|
16
|
+
if (isRecord(part) && typeof part.text === "string") text += part.text;
|
|
17
|
+
}
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
function namedSession(extra) {
|
|
21
|
+
if (typeof extra === "string") return extra.length > 0 ? extra : void 0;
|
|
22
|
+
if (!isRecord(extra)) return void 0;
|
|
23
|
+
const id = extra.session_id;
|
|
24
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
function sessionOf(body) {
|
|
28
|
+
if (typeof body.session_id === "string" && body.session_id.length > 0) return body.session_id;
|
|
29
|
+
for (const name of extraNames) {
|
|
30
|
+
const key = namedSession(body[name]);
|
|
31
|
+
if (key !== void 0) return key;
|
|
32
|
+
}
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
function toolNames(body) {
|
|
36
|
+
const names = /* @__PURE__ */ new Set();
|
|
37
|
+
if (!Array.isArray(body.tools)) return names;
|
|
38
|
+
for (const tool of body.tools) {
|
|
39
|
+
if (!isRecord(tool)) continue;
|
|
40
|
+
const fn = tool.function;
|
|
41
|
+
if (!isRecord(fn)) continue;
|
|
42
|
+
if (typeof fn.name !== "string" || fn.name.length === 0) continue;
|
|
43
|
+
names.add(fn.name);
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
47
|
+
function lastTurn(messages) {
|
|
48
|
+
if (!Array.isArray(messages)) return void 0;
|
|
49
|
+
let prior;
|
|
50
|
+
let priorTools = false;
|
|
51
|
+
let user;
|
|
52
|
+
for (const message of messages) {
|
|
53
|
+
if (!isRecord(message) || message.role === "system") continue;
|
|
54
|
+
if (message.role === "assistant") {
|
|
55
|
+
prior = textOf(message.content);
|
|
56
|
+
priorTools = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
57
|
+
user = void 0;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (message.role === "user" || message.role === "tool") user = textOf(message.content);
|
|
61
|
+
}
|
|
62
|
+
return { prior, priorTools, user };
|
|
63
|
+
}
|
|
64
|
+
function parseTurn(body) {
|
|
65
|
+
if (!isRecord(body)) return "invalid chat completion";
|
|
66
|
+
if (body.stream !== true) return "stream is required";
|
|
67
|
+
const sessionKey = sessionOf(body);
|
|
68
|
+
if (sessionKey === void 0) return "missing session id";
|
|
69
|
+
const turn = lastTurn(body.messages);
|
|
70
|
+
if (turn?.user === void 0) return "missing user turn";
|
|
71
|
+
const model = typeof body.model === "string" && body.model.length > 0 ? body.model : "brain";
|
|
72
|
+
return {
|
|
73
|
+
model,
|
|
74
|
+
sessionKey,
|
|
75
|
+
tools: toolNames(body),
|
|
76
|
+
prior: turn.prior,
|
|
77
|
+
priorTools: turn.priorTools,
|
|
78
|
+
user: turn.user
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function sameReply(memory, prior) {
|
|
82
|
+
if (prior === void 0) return false;
|
|
83
|
+
return replyHash(prior) === memory.hash;
|
|
84
|
+
}
|
|
85
|
+
function turnPlan(memory, turn) {
|
|
86
|
+
if (memory === void 0) return { replay: false, content: turn.user };
|
|
87
|
+
const matches = sameReply(memory, turn.prior);
|
|
88
|
+
if (turn.user === memory.user && !matches && !turn.priorTools) {
|
|
89
|
+
return { replay: true, text: memory.text };
|
|
90
|
+
}
|
|
91
|
+
if (!matches && turn.prior !== void 0 && !turn.priorTools) {
|
|
92
|
+
return { replay: false, content: `The caller heard: ${turn.prior}
|
|
93
|
+
|
|
94
|
+
${turn.user}` };
|
|
95
|
+
}
|
|
96
|
+
return { replay: false, content: turn.user };
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
parseTurn,
|
|
100
|
+
turnPlan
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/parse.ts"],
|
|
4
|
+
"sourcesContent": ["import { replyHash } from './hash.ts';\n\nexport interface Memory {\n hash: string;\n text: string;\n user: string;\n}\n\nexport interface ParsedTurn {\n model: string;\n sessionKey: string;\n tools: Set<string>;\n prior?: string;\n priorTools: boolean;\n user: string;\n}\n\nexport type TurnPlan = { replay: true; text: string } | { replay: false; content: string };\n\nconst extraNames = [\n 'fishaudio_extra_body',\n 'llm_extra_body',\n 'elevenlabs_extra_body',\n 'custom_llm_extra_body',\n] as const;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction textOf(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n let text = '';\n for (const part of content) {\n if (isRecord(part) && typeof part.text === 'string') text += part.text;\n }\n return text;\n}\n\nfunction namedSession(extra: unknown): string | undefined {\n if (typeof extra === 'string') return extra.length > 0 ? extra : undefined;\n if (!isRecord(extra)) return undefined;\n const id = extra.session_id;\n if (typeof id === 'string' && id.length > 0) return id;\n return undefined;\n}\n\nfunction sessionOf(body: Record<string, unknown>): string | undefined {\n if (typeof body.session_id === 'string' && body.session_id.length > 0) return body.session_id;\n for (const name of extraNames) {\n const key = namedSession(body[name]);\n if (key !== undefined) return key;\n }\n return undefined;\n}\n\nfunction toolNames(body: Record<string, unknown>): Set<string> {\n const names = new Set<string>();\n if (!Array.isArray(body.tools)) return names;\n for (const tool of body.tools) {\n if (!isRecord(tool)) continue;\n const fn = tool.function;\n if (!isRecord(fn)) continue;\n if (typeof fn.name !== 'string' || fn.name.length === 0) continue;\n names.add(fn.name);\n }\n return names;\n}\n\nfunction lastTurn(\n messages: unknown\n): { prior?: string; priorTools: boolean; user?: string } | undefined {\n if (!Array.isArray(messages)) return undefined;\n let prior: string | undefined;\n let priorTools = false;\n let user: string | undefined;\n for (const message of messages) {\n if (!isRecord(message) || message.role === 'system') continue;\n if (message.role === 'assistant') {\n prior = textOf(message.content);\n priorTools = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;\n user = undefined;\n continue;\n }\n if (message.role === 'user' || message.role === 'tool') user = textOf(message.content);\n }\n return { prior, priorTools, user };\n}\n\nexport function parseTurn(body: unknown): ParsedTurn | string {\n if (!isRecord(body)) return 'invalid chat completion';\n if (body.stream !== true) return 'stream is required';\n const sessionKey = sessionOf(body);\n if (sessionKey === undefined) return 'missing session id';\n const turn = lastTurn(body.messages);\n if (turn?.user === undefined) return 'missing user turn';\n const model = typeof body.model === 'string' && body.model.length > 0 ? body.model : 'brain';\n return {\n model,\n sessionKey,\n tools: toolNames(body),\n prior: turn.prior,\n priorTools: turn.priorTools,\n user: turn.user,\n };\n}\n\nfunction sameReply(memory: Memory, prior: string | undefined): boolean {\n if (prior === undefined) return false;\n return replyHash(prior) === memory.hash;\n}\n\nexport function turnPlan(memory: Memory | undefined, turn: ParsedTurn): TurnPlan {\n if (memory === undefined) return { replay: false, content: turn.user };\n const matches = sameReply(memory, turn.prior);\n if (turn.user === memory.user && !matches && !turn.priorTools) {\n return { replay: true, text: memory.text };\n }\n if (!matches && turn.prior !== undefined && !turn.priorTools) {\n return { replay: false, content: `The caller heard: ${turn.prior}\\n\\n${turn.user}` };\n }\n return { replay: false, content: turn.user };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function sseChunk(model, created, delta, finish) {
|
|
2
|
+
return `data: ${JSON.stringify({
|
|
3
|
+
id: "chatcmpl-brain",
|
|
4
|
+
object: "chat.completion.chunk",
|
|
5
|
+
created,
|
|
6
|
+
model,
|
|
7
|
+
choices: [{ index: 0, delta, finish_reason: finish }]
|
|
8
|
+
})}
|
|
9
|
+
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
12
|
+
function sseDone() {
|
|
13
|
+
return "data: [DONE]\n\n";
|
|
14
|
+
}
|
|
15
|
+
function sseError(message) {
|
|
16
|
+
return `data: ${JSON.stringify({ error: { message } })}
|
|
17
|
+
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
sseChunk,
|
|
22
|
+
sseDone,
|
|
23
|
+
sseError
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/sse.ts"],
|
|
4
|
+
"sourcesContent": ["export function sseChunk(\n model: string,\n created: number,\n delta: Record<string, unknown>,\n finish: string | null\n): string {\n return `data: ${JSON.stringify({\n id: 'chatcmpl-brain',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta, finish_reason: finish }],\n })}\\n\\n`;\n}\n\nexport function sseDone(): string {\n return 'data: [DONE]\\n\\n';\n}\n\nexport function sseError(message: string): string {\n return `data: ${JSON.stringify({ error: { message } })}\\n\\n`;\n}\n"],
|
|
5
|
+
"mappings": "AAAO,SAAS,SACd,OACA,SACA,OACA,QACQ;AACR,SAAO,SAAS,KAAK,UAAU;AAAA,IAC7B,IAAI;AAAA,IACJ,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,eAAe,OAAO,CAAC;AAAA,EACtD,CAAC,CAAC;AAAA;AAAA;AACJ;AAEO,SAAS,UAAkB;AAChC,SAAO;AACT;AAEO,SAAS,SAAS,SAAyB;AAChD,SAAO,SAAS,KAAK,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;AAAA;AAAA;AACxD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSpokenState,
|
|
3
|
+
spokenPieces
|
|
4
|
+
} from "../spoken.js";
|
|
5
|
+
import { vendorCall } from "./call.js";
|
|
6
|
+
import { replyHash } from "./hash.js";
|
|
7
|
+
import { sseChunk, sseDone, sseError } from "./sse.js";
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
function sender(model, created, clock, controller) {
|
|
10
|
+
return (delta, finish2) => {
|
|
11
|
+
const body = clock.opened ? delta : { role: "assistant", ...delta };
|
|
12
|
+
clock.opened = true;
|
|
13
|
+
controller.enqueue(encoder.encode(sseChunk(model, created, body, finish2)));
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function finish(send, controller, reason) {
|
|
17
|
+
send({}, reason);
|
|
18
|
+
controller.enqueue(encoder.encode(sseDone()));
|
|
19
|
+
}
|
|
20
|
+
function remember(sessions, parsed, clock) {
|
|
21
|
+
sessions.set(parsed.sessionKey, {
|
|
22
|
+
hash: replyHash(clock.streamed),
|
|
23
|
+
text: clock.streamed,
|
|
24
|
+
user: parsed.user
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function writeReplay(parsed, text, sessions, controller) {
|
|
28
|
+
const clock = { cause: void 0, sent: false, streamed: text, opened: false };
|
|
29
|
+
const send = sender(parsed.model, Math.floor(Date.now() / 1e3), clock, controller);
|
|
30
|
+
if (text.length > 0) send({ content: text }, null);
|
|
31
|
+
finish(send, controller, "stop");
|
|
32
|
+
remember(sessions, parsed, clock);
|
|
33
|
+
}
|
|
34
|
+
function bindAbort(request, control, clock, hardMs) {
|
|
35
|
+
const fromClient = () => {
|
|
36
|
+
clock.cause = clock.cause ?? "client";
|
|
37
|
+
control.abort();
|
|
38
|
+
};
|
|
39
|
+
if (request.signal.aborted) fromClient();
|
|
40
|
+
else request.signal.addEventListener("abort", fromClient, { once: true });
|
|
41
|
+
const deadline = setTimeout(() => {
|
|
42
|
+
clock.cause = clock.cause ?? "deadline";
|
|
43
|
+
control.abort();
|
|
44
|
+
}, hardMs);
|
|
45
|
+
return () => {
|
|
46
|
+
clearTimeout(deadline);
|
|
47
|
+
request.signal.removeEventListener("abort", fromClient);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function armFiller(options, message, clock, send) {
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
if (clock.sent) return;
|
|
53
|
+
const text = options.filler(message);
|
|
54
|
+
if (text.length === 0) return;
|
|
55
|
+
clock.sent = true;
|
|
56
|
+
clock.streamed += text;
|
|
57
|
+
send({ content: text }, null);
|
|
58
|
+
}, options.budget.firstTokenMs);
|
|
59
|
+
return () => clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
function writeAnswers(event, state, clock, send) {
|
|
62
|
+
for (const piece of spokenPieces(event, state)) {
|
|
63
|
+
clock.sent = true;
|
|
64
|
+
clock.streamed += piece.text;
|
|
65
|
+
send({ content: piece.text }, null);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function writeTool(send, call) {
|
|
69
|
+
send(
|
|
70
|
+
{
|
|
71
|
+
tool_calls: [
|
|
72
|
+
{
|
|
73
|
+
index: 0,
|
|
74
|
+
id: call.id,
|
|
75
|
+
type: "function",
|
|
76
|
+
function: { name: call.name, arguments: call.arguments }
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
null
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function stopFor(clock, controller) {
|
|
84
|
+
if (clock.cause === "deadline") controller.enqueue(encoder.encode(sseError("llm deadline")));
|
|
85
|
+
}
|
|
86
|
+
function errorText(error) {
|
|
87
|
+
if (error instanceof Error) return error.message;
|
|
88
|
+
return String(error);
|
|
89
|
+
}
|
|
90
|
+
async function liveTurn(options, sessions, parsed, content, request, controller) {
|
|
91
|
+
const clock = { cause: void 0, sent: false, streamed: "", opened: false };
|
|
92
|
+
const send = sender(parsed.model, Math.floor(Date.now() / 1e3), clock, controller);
|
|
93
|
+
const message = { role: "user", content, timestamp: Date.now() };
|
|
94
|
+
const control = new AbortController();
|
|
95
|
+
const releaseAbort = bindAbort(request, control, clock, options.budget.hardDeadlineMs);
|
|
96
|
+
const releaseFiller = armFiller(options, message, clock, send);
|
|
97
|
+
try {
|
|
98
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
99
|
+
const conversation = await options.conversation(parsed.sessionKey, control.signal);
|
|
100
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
101
|
+
const state = createSpokenState();
|
|
102
|
+
const meta = { source: "voice", transcriptionModel: parsed.model };
|
|
103
|
+
for await (const event of conversation.prompt(message, meta, control.signal)) {
|
|
104
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
105
|
+
writeAnswers(event, state, clock, send);
|
|
106
|
+
const call = vendorCall(event, parsed.tools);
|
|
107
|
+
if (call === void 0 || options.vendorTools === "drop") continue;
|
|
108
|
+
writeTool(send, call);
|
|
109
|
+
finish(send, controller, "tool_calls");
|
|
110
|
+
remember(sessions, parsed, clock);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (clock.cause !== void 0) return stopFor(clock, controller);
|
|
114
|
+
finish(send, controller, "stop");
|
|
115
|
+
remember(sessions, parsed, clock);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (clock.cause === "client") return;
|
|
118
|
+
const text = clock.cause === "deadline" ? "llm deadline" : errorText(error);
|
|
119
|
+
controller.enqueue(encoder.encode(sseError(text)));
|
|
120
|
+
} finally {
|
|
121
|
+
releaseFiller();
|
|
122
|
+
releaseAbort();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function runTurn(options, sessions, parsed, plan, request, controller) {
|
|
126
|
+
if (plan.replay) {
|
|
127
|
+
writeReplay(parsed, plan.text, sessions, controller);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await liveTurn(options, sessions, parsed, plan.content, request, controller);
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
runTurn
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=turn.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/brain/turn.ts"],
|
|
4
|
+
"sourcesContent": ["import type { UserMessage } from '@earendil-works/pi-ai';\nimport {\n createSpokenState,\n type SpokenState,\n spokenPieces,\n type VoiceAgentEvent,\n} from '../spoken.ts';\nimport { type VendorCall, vendorCall } from './call.ts';\nimport { replyHash } from './hash.ts';\nimport type { BrainOptions } from './options.ts';\nimport type { Memory, ParsedTurn, TurnPlan } from './parse.ts';\nimport { sseChunk, sseDone, sseError } from './sse.ts';\n\nconst encoder = new TextEncoder();\n\ninterface Clock {\n cause: 'client' | 'deadline' | undefined;\n sent: boolean;\n streamed: string;\n opened: boolean;\n}\n\ntype Send = (delta: Record<string, unknown>, finish: string | null) => void;\ntype Controller = ReadableStreamDefaultController<Uint8Array>;\n\nfunction sender(model: string, created: number, clock: Clock, controller: Controller): Send {\n return (delta, finish) => {\n const body = clock.opened ? delta : { role: 'assistant', ...delta };\n clock.opened = true;\n controller.enqueue(encoder.encode(sseChunk(model, created, body, finish)));\n };\n}\n\nfunction finish(send: Send, controller: Controller, reason: string): void {\n send({}, reason);\n controller.enqueue(encoder.encode(sseDone()));\n}\n\nfunction remember(sessions: Map<string, Memory>, parsed: ParsedTurn, clock: Clock): void {\n sessions.set(parsed.sessionKey, {\n hash: replyHash(clock.streamed),\n text: clock.streamed,\n user: parsed.user,\n });\n}\n\nfunction writeReplay(\n parsed: ParsedTurn,\n text: string,\n sessions: Map<string, Memory>,\n controller: Controller\n): void {\n const clock: Clock = { cause: undefined, sent: false, streamed: text, opened: false };\n const send = sender(parsed.model, Math.floor(Date.now() / 1000), clock, controller);\n if (text.length > 0) send({ content: text }, null);\n finish(send, controller, 'stop');\n remember(sessions, parsed, clock);\n}\n\nfunction bindAbort(\n request: Request,\n control: AbortController,\n clock: Clock,\n hardMs: number\n): () => void {\n const fromClient = () => {\n clock.cause = clock.cause ?? 'client';\n control.abort();\n };\n if (request.signal.aborted) fromClient();\n else request.signal.addEventListener('abort', fromClient, { once: true });\n const deadline = setTimeout(() => {\n clock.cause = clock.cause ?? 'deadline';\n control.abort();\n }, hardMs);\n return () => {\n clearTimeout(deadline);\n request.signal.removeEventListener('abort', fromClient);\n };\n}\n\nfunction armFiller(\n options: BrainOptions,\n message: UserMessage,\n clock: Clock,\n send: Send\n): () => void {\n const timer = setTimeout(() => {\n if (clock.sent) return;\n const text = options.filler(message);\n if (text.length === 0) return;\n clock.sent = true;\n clock.streamed += text;\n send({ content: text }, null);\n }, options.budget.firstTokenMs);\n return () => clearTimeout(timer);\n}\n\nfunction writeAnswers(event: VoiceAgentEvent, state: SpokenState, clock: Clock, send: Send): void {\n for (const piece of spokenPieces(event, state)) {\n clock.sent = true;\n clock.streamed += piece.text;\n send({ content: piece.text }, null);\n }\n}\n\nfunction writeTool(send: Send, call: VendorCall): void {\n send(\n {\n tool_calls: [\n {\n index: 0,\n id: call.id,\n type: 'function',\n function: { name: call.name, arguments: call.arguments },\n },\n ],\n },\n null\n );\n}\n\nfunction stopFor(clock: Clock, controller: Controller): void {\n if (clock.cause === 'deadline') controller.enqueue(encoder.encode(sseError('llm deadline')));\n}\n\nfunction errorText(error: unknown): string {\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\nasync function liveTurn(\n options: BrainOptions,\n sessions: Map<string, Memory>,\n parsed: ParsedTurn,\n content: string,\n request: Request,\n controller: Controller\n): Promise<void> {\n const clock: Clock = { cause: undefined, sent: false, streamed: '', opened: false };\n const send = sender(parsed.model, Math.floor(Date.now() / 1000), clock, controller);\n const message: UserMessage = { role: 'user', content, timestamp: Date.now() };\n const control = new AbortController();\n const releaseAbort = bindAbort(request, control, clock, options.budget.hardDeadlineMs);\n const releaseFiller = armFiller(options, message, clock, send);\n try {\n if (clock.cause !== undefined) return stopFor(clock, controller);\n const conversation = await options.conversation(parsed.sessionKey, control.signal);\n if (clock.cause !== undefined) return stopFor(clock, controller);\n const state = createSpokenState();\n const meta = { source: 'voice' as const, transcriptionModel: parsed.model };\n for await (const event of conversation.prompt(message, meta, control.signal)) {\n if (clock.cause !== undefined) return stopFor(clock, controller);\n writeAnswers(event, state, clock, send);\n const call = vendorCall(event, parsed.tools);\n if (call === undefined || options.vendorTools === 'drop') continue;\n writeTool(send, call);\n finish(send, controller, 'tool_calls');\n remember(sessions, parsed, clock);\n return;\n }\n if (clock.cause !== undefined) return stopFor(clock, controller);\n finish(send, controller, 'stop');\n remember(sessions, parsed, clock);\n } catch (error) {\n if (clock.cause === 'client') return;\n const text = clock.cause === 'deadline' ? 'llm deadline' : errorText(error);\n controller.enqueue(encoder.encode(sseError(text)));\n } finally {\n releaseFiller();\n releaseAbort();\n }\n}\n\nexport async function runTurn(\n options: BrainOptions,\n sessions: Map<string, Memory>,\n parsed: ParsedTurn,\n plan: TurnPlan,\n request: Request,\n controller: Controller\n): Promise<void> {\n if (plan.replay) {\n writeReplay(parsed, plan.text, sessions, controller);\n return;\n }\n await liveTurn(options, sessions, parsed, plan.content, request, controller);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["finish"]
|
|
7
|
+
}
|
package/dist/echo.js
CHANGED
|
@@ -58,7 +58,7 @@ function measureEcho(reference, mic, sampleRate) {
|
|
|
58
58
|
recommendation: recommend(residualDb)
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
async function calibrateEcho(out, mic, probe, durationMs, signal) {
|
|
61
|
+
async function calibrateEcho(out, mic, probe, durationMs, signal, canceller) {
|
|
62
62
|
if (signal.aborted) throw new VoiceRequestError("aborted", "aborted");
|
|
63
63
|
out.bed.play(probe, 0);
|
|
64
64
|
const rate = { value: 0 };
|
|
@@ -68,11 +68,55 @@ async function calibrateEcho(out, mic, probe, durationMs, signal) {
|
|
|
68
68
|
const pair = await Promise.all([heard, reference]);
|
|
69
69
|
let used = rate.value;
|
|
70
70
|
if (used <= 0) used = 1;
|
|
71
|
-
|
|
71
|
+
const cleaned = applyCanceller(pair[0], pair[1], used, canceller);
|
|
72
|
+
return withStats(measureEcho(pair[1], cleaned.samples, used), cleaned.stats);
|
|
72
73
|
} finally {
|
|
73
74
|
out.bed.stop(0);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
function applyCanceller(mic, reference, sampleRate, canceller) {
|
|
78
|
+
if (canceller === void 0) return { samples: mic };
|
|
79
|
+
const cancelled = canceller.cancel(
|
|
80
|
+
floatChunk(mic, sampleRate),
|
|
81
|
+
floatChunk(reference, sampleRate)
|
|
82
|
+
);
|
|
83
|
+
return { samples: heardSamples(cancelled, mic), stats: readStats(canceller) };
|
|
84
|
+
}
|
|
85
|
+
function readStats(canceller) {
|
|
86
|
+
if (canceller.stats === void 0) return void 0;
|
|
87
|
+
return canceller.stats();
|
|
88
|
+
}
|
|
89
|
+
function heardSamples(chunk, fallback) {
|
|
90
|
+
const decoded = samplesOf(chunk);
|
|
91
|
+
if (decoded.length === 0) return fallback;
|
|
92
|
+
return Float32Array.from(decoded);
|
|
93
|
+
}
|
|
94
|
+
function floatChunk(samples, sampleRate) {
|
|
95
|
+
const data = new Uint8Array(samples.length * 4);
|
|
96
|
+
const view = new DataView(data.buffer);
|
|
97
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
98
|
+
view.setFloat32(index * 4, samples[index], true);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
data,
|
|
102
|
+
format: { encoding: "pcmf32", sampleRate, channels: 1 },
|
|
103
|
+
timestamp: 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function withStats(report, stats) {
|
|
107
|
+
if (stats === void 0) return report;
|
|
108
|
+
return {
|
|
109
|
+
delayMs: chosen(stats.delayMs, report.delayMs),
|
|
110
|
+
cancellationDb: chosen(stats.cancellationDb, report.cancellationDb),
|
|
111
|
+
residualDb: report.residualDb,
|
|
112
|
+
recommendation: report.recommendation
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function chosen(value, fallback) {
|
|
116
|
+
if (value === null) return fallback;
|
|
117
|
+
if (!Number.isFinite(value)) return fallback;
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
76
120
|
function quiet() {
|
|
77
121
|
return { delayMs: 0, cancellationDb: 80, residualDb: -80, recommendation: "interrupt" };
|
|
78
122
|
}
|
package/dist/echo.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/echo.ts"],
|
|
4
|
-
"sourcesContent": ["import { type AudioChunk, type AudioSource, chunkSeconds } from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { AudioOut, Cue } from './soundscape.ts';\nimport type { EchoRecommendation } from './turns.ts';\n\nexport type EchoMode = 'browser' | 'wasm' | 'none';\n\nexport interface EchoCanceller {\n cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk;\n}\n\nexport interface EchoControl {\n mode: EchoMode;\n reference: AudioSource;\n canceller?: EchoCanceller;\n}\n\nexport interface EchoReport {\n delayMs: number;\n cancellationDb: number;\n residualDb: number;\n recommendation: EchoRecommendation;\n}\n\nexport interface MicOpen {\n echoCancellation: boolean | 'all';\n playback: 'webaudio' | 'rtc-loopback';\n}\n\nconst floor = 1e-8;\n\nexport function micOpen(mode: EchoMode, useAll: boolean): MicOpen {\n if (mode === 'browser') {\n let echoCancellation: boolean | 'all' = true;\n if (useAll) echoCancellation = 'all';\n return { echoCancellation, playback: 'rtc-loopback' };\n }\n return { echoCancellation: false, playback: 'webaudio' };\n}\n\nexport function echoRuntime(hasMediaDevices: boolean): 'node' | 'browser' {\n if (hasMediaDevices) return 'browser';\n return 'node';\n}\n\nexport function assertNodeEcho(mode: EchoMode, runtime: 'node' | 'browser'): void {\n if (runtime === 'node' && mode === 'browser') {\n throw new VoiceRequestError('error', 'Node echo control is wasm or none');\n }\n}\n\nexport function chooseCapture(tried: EchoMode, report: EchoReport): EchoMode {\n if (tried === 'none') return 'none';\n if (tried === 'wasm') return 'wasm';\n if (report.cancellationDb < 12) return 'wasm';\n return 'browser';\n}\n\nexport function openEchoMic(control: EchoControl, mic: AudioSource): AudioSource {\n if (control.mode === 'none' || control.mode === 'browser') return mic;\n const canceller = control.canceller;\n if (canceller === undefined) {\n throw new VoiceRequestError('error', 'wasm echo control needs a canceller');\n }\n return cancelStream(mic, control.reference, canceller);\n}\n\nexport function measureEcho(\n reference: Float32Array,\n mic: Float32Array,\n sampleRate: number\n): EchoReport {\n if (reference.length === 0) return quiet();\n if (mic.length === 0) return quiet();\n let rate = sampleRate;\n if (rate <= 0) rate = 1;\n const maxLag = Math.min(mic.length - 1, Math.max(0, Math.round(rate * 0.25)));\n let bestLag = 0;\n let best = -1;\n let bestGain = 0;\n for (let lag = 0; lag <= maxLag; lag += 1) {\n const scored = scoreLag(reference, mic, lag);\n if (scored.score > best) {\n best = scored.score;\n bestLag = lag;\n bestGain = scored.gain;\n }\n }\n const leak = Math.abs(bestGain);\n const residualDb = amplitudeDb(leak);\n return {\n delayMs: (bestLag / rate) * 1000,\n cancellationDb: -residualDb,\n residualDb,\n recommendation: recommend(residualDb),\n };\n}\n\nexport async function calibrateEcho(\n out: AudioOut,\n mic: AudioSource,\n probe: Cue,\n durationMs: number,\n signal: AbortSignal\n): Promise<EchoReport> {\n if (signal.aborted) throw new VoiceRequestError('aborted', 'aborted');\n out.bed.play(probe, 0);\n const rate = { value: 0 };\n try {\n const heard = readSamples(mic, durationMs, signal, rate);\n const reference = readSamples(out.reference, durationMs, signal, rate);\n const pair = await Promise.all([heard, reference]);\n let used = rate.value;\n if (used <= 0) used = 1;\n
|
|
5
|
-
"mappings": "AAAA,SAA4C,oBAAoB;AAChE,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { type AudioChunk, type AudioSource, chunkSeconds } from './audio.ts';\nimport { VoiceRequestError } from './errors.ts';\nimport type { AudioOut, Cue } from './soundscape.ts';\nimport type { EchoRecommendation } from './turns.ts';\n\nexport type EchoMode = 'browser' | 'wasm' | 'none';\n\nexport interface EchoStats {\n delayMs: number | null;\n cancellationDb: number | null;\n}\n\nexport interface EchoCanceller {\n cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk;\n stats?(): EchoStats;\n}\n\nexport interface EchoControl {\n mode: EchoMode;\n reference: AudioSource;\n canceller?: EchoCanceller;\n}\n\nexport interface EchoReport {\n delayMs: number;\n cancellationDb: number;\n residualDb: number;\n recommendation: EchoRecommendation;\n}\n\nexport interface MicOpen {\n echoCancellation: boolean | 'all';\n playback: 'webaudio' | 'rtc-loopback';\n}\n\nconst floor = 1e-8;\n\nexport function micOpen(mode: EchoMode, useAll: boolean): MicOpen {\n if (mode === 'browser') {\n let echoCancellation: boolean | 'all' = true;\n if (useAll) echoCancellation = 'all';\n return { echoCancellation, playback: 'rtc-loopback' };\n }\n return { echoCancellation: false, playback: 'webaudio' };\n}\n\nexport function echoRuntime(hasMediaDevices: boolean): 'node' | 'browser' {\n if (hasMediaDevices) return 'browser';\n return 'node';\n}\n\nexport function assertNodeEcho(mode: EchoMode, runtime: 'node' | 'browser'): void {\n if (runtime === 'node' && mode === 'browser') {\n throw new VoiceRequestError('error', 'Node echo control is wasm or none');\n }\n}\n\nexport function chooseCapture(tried: EchoMode, report: EchoReport): EchoMode {\n if (tried === 'none') return 'none';\n if (tried === 'wasm') return 'wasm';\n if (report.cancellationDb < 12) return 'wasm';\n return 'browser';\n}\n\nexport function openEchoMic(control: EchoControl, mic: AudioSource): AudioSource {\n if (control.mode === 'none' || control.mode === 'browser') return mic;\n const canceller = control.canceller;\n if (canceller === undefined) {\n throw new VoiceRequestError('error', 'wasm echo control needs a canceller');\n }\n return cancelStream(mic, control.reference, canceller);\n}\n\nexport function measureEcho(\n reference: Float32Array,\n mic: Float32Array,\n sampleRate: number\n): EchoReport {\n if (reference.length === 0) return quiet();\n if (mic.length === 0) return quiet();\n let rate = sampleRate;\n if (rate <= 0) rate = 1;\n const maxLag = Math.min(mic.length - 1, Math.max(0, Math.round(rate * 0.25)));\n let bestLag = 0;\n let best = -1;\n let bestGain = 0;\n for (let lag = 0; lag <= maxLag; lag += 1) {\n const scored = scoreLag(reference, mic, lag);\n if (scored.score > best) {\n best = scored.score;\n bestLag = lag;\n bestGain = scored.gain;\n }\n }\n const leak = Math.abs(bestGain);\n const residualDb = amplitudeDb(leak);\n return {\n delayMs: (bestLag / rate) * 1000,\n cancellationDb: -residualDb,\n residualDb,\n recommendation: recommend(residualDb),\n };\n}\n\nexport async function calibrateEcho(\n out: AudioOut,\n mic: AudioSource,\n probe: Cue,\n durationMs: number,\n signal: AbortSignal,\n canceller?: EchoCanceller\n): Promise<EchoReport> {\n if (signal.aborted) throw new VoiceRequestError('aborted', 'aborted');\n out.bed.play(probe, 0);\n const rate = { value: 0 };\n try {\n const heard = readSamples(mic, durationMs, signal, rate);\n const reference = readSamples(out.reference, durationMs, signal, rate);\n const pair = await Promise.all([heard, reference]);\n let used = rate.value;\n if (used <= 0) used = 1;\n const cleaned = applyCanceller(pair[0], pair[1], used, canceller);\n return withStats(measureEcho(pair[1], cleaned.samples, used), cleaned.stats);\n } finally {\n out.bed.stop(0);\n }\n}\n\nfunction applyCanceller(\n mic: Float32Array,\n reference: Float32Array,\n sampleRate: number,\n canceller: EchoCanceller | undefined\n): { samples: Float32Array; stats?: EchoStats } {\n if (canceller === undefined) return { samples: mic };\n const cancelled = canceller.cancel(\n floatChunk(mic, sampleRate),\n floatChunk(reference, sampleRate)\n );\n return { samples: heardSamples(cancelled, mic), stats: readStats(canceller) };\n}\n\nfunction readStats(canceller: EchoCanceller): EchoStats | undefined {\n if (canceller.stats === undefined) return undefined;\n return canceller.stats();\n}\n\nfunction heardSamples(chunk: AudioChunk, fallback: Float32Array): Float32Array {\n const decoded = samplesOf(chunk);\n if (decoded.length === 0) return fallback;\n return Float32Array.from(decoded);\n}\n\nfunction floatChunk(samples: Float32Array, sampleRate: number): AudioChunk {\n const data = new Uint8Array(samples.length * 4);\n const view = new DataView(data.buffer);\n for (let index = 0; index < samples.length; index += 1) {\n view.setFloat32(index * 4, samples[index] as number, true);\n }\n return {\n data,\n format: { encoding: 'pcmf32', sampleRate, channels: 1 },\n timestamp: 0,\n };\n}\n\nfunction withStats(report: EchoReport, stats: EchoStats | undefined): EchoReport {\n if (stats === undefined) return report;\n return {\n delayMs: chosen(stats.delayMs, report.delayMs),\n cancellationDb: chosen(stats.cancellationDb, report.cancellationDb),\n residualDb: report.residualDb,\n recommendation: report.recommendation,\n };\n}\n\nfunction chosen(value: number | null, fallback: number): number {\n if (value === null) return fallback;\n if (!Number.isFinite(value)) return fallback;\n return value;\n}\n\nfunction quiet(): EchoReport {\n return { delayMs: 0, cancellationDb: 80, residualDb: -80, recommendation: 'interrupt' };\n}\n\nfunction recommend(residualDb: number): EchoRecommendation {\n if (residualDb <= -30) return 'interrupt';\n if (residualDb <= -12) return 'onWords';\n return 'headphones';\n}\n\nfunction scoreLag(\n reference: Float32Array,\n mic: Float32Array,\n lag: number\n): { score: number; gain: number } {\n let dot = 0;\n let energy = 0;\n const length = Math.min(reference.length, mic.length - lag);\n for (let index = 0; index < length; index += 1) {\n const ref = reference[index] as number;\n const heard = mic[index + lag] as number;\n dot += ref * heard;\n energy += ref * ref;\n }\n if (energy <= floor) return { score: -1, gain: 0 };\n return { score: Math.abs(dot), gain: dot / energy };\n}\n\nfunction amplitudeDb(amplitude: number): number {\n let safe = amplitude;\n if (safe <= floor) safe = floor;\n return 20 * Math.log10(safe);\n}\n\nasync function* cancelStream(\n mic: AudioSource,\n reference: AudioSource,\n canceller: EchoCanceller\n): AudioSource {\n const refs = reference[Symbol.asyncIterator]();\n for await (const chunk of mic) {\n const next = await refs.next();\n if (next.done) {\n yield chunk;\n continue;\n }\n yield canceller.cancel(chunk, next.value);\n }\n}\n\nasync function readSamples(\n source: AudioSource,\n durationMs: number,\n signal: AbortSignal,\n rate: { value: number }\n): Promise<Float32Array> {\n const samples: number[] = [];\n let heardMs = 0;\n for await (const chunk of source) {\n if (signal.aborted) throw new VoiceRequestError('aborted', 'aborted');\n if (rate.value === 0) rate.value = chunk.format.sampleRate;\n for (const sample of samplesOf(chunk)) samples.push(sample);\n heardMs += chunkSeconds(chunk) * 1000;\n if (heardMs >= durationMs) break;\n }\n return Float32Array.from(samples);\n}\n\nfunction samplesOf(chunk: AudioChunk): number[] {\n if (chunk.format.encoding === 'pcm16') return pcm16Samples(chunk.data);\n if (chunk.format.encoding === 'pcmf32') return pcmf32Samples(chunk.data);\n return [];\n}\n\nfunction pcm16Samples(data: Uint8Array): number[] {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const samples: number[] = [];\n const count = Math.floor(data.byteLength / 2);\n for (let index = 0; index < count; index += 1) {\n samples.push(view.getInt16(index * 2, true) / 32768);\n }\n return samples;\n}\n\nfunction pcmf32Samples(data: Uint8Array): number[] {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const samples: number[] = [];\n const count = Math.floor(data.byteLength / 4);\n for (let index = 0; index < count; index += 1) {\n samples.push(view.getFloat32(index * 4, true));\n }\n return samples;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAA4C,oBAAoB;AAChE,SAAS,yBAAyB;AAkClC,MAAM,QAAQ;AAEP,SAAS,QAAQ,MAAgB,QAA0B;AAChE,MAAI,SAAS,WAAW;AACtB,QAAI,mBAAoC;AACxC,QAAI,OAAQ,oBAAmB;AAC/B,WAAO,EAAE,kBAAkB,UAAU,eAAe;AAAA,EACtD;AACA,SAAO,EAAE,kBAAkB,OAAO,UAAU,WAAW;AACzD;AAEO,SAAS,YAAY,iBAA8C;AACxE,MAAI,gBAAiB,QAAO;AAC5B,SAAO;AACT;AAEO,SAAS,eAAe,MAAgB,SAAmC;AAChF,MAAI,YAAY,UAAU,SAAS,WAAW;AAC5C,UAAM,IAAI,kBAAkB,SAAS,mCAAmC;AAAA,EAC1E;AACF;AAEO,SAAS,cAAc,OAAiB,QAA8B;AAC3E,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,OAAO,iBAAiB,GAAI,QAAO;AACvC,SAAO;AACT;AAEO,SAAS,YAAY,SAAsB,KAA+B;AAC/E,MAAI,QAAQ,SAAS,UAAU,QAAQ,SAAS,UAAW,QAAO;AAClE,QAAM,YAAY,QAAQ;AAC1B,MAAI,cAAc,QAAW;AAC3B,UAAM,IAAI,kBAAkB,SAAS,qCAAqC;AAAA,EAC5E;AACA,SAAO,aAAa,KAAK,QAAQ,WAAW,SAAS;AACvD;AAEO,SAAS,YACd,WACA,KACA,YACY;AACZ,MAAI,UAAU,WAAW,EAAG,QAAO,MAAM;AACzC,MAAI,IAAI,WAAW,EAAG,QAAO,MAAM;AACnC,MAAI,OAAO;AACX,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,SAAS,KAAK,IAAI,IAAI,SAAS,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,IAAI,CAAC,CAAC;AAC5E,MAAI,UAAU;AACd,MAAI,OAAO;AACX,MAAI,WAAW;AACf,WAAS,MAAM,GAAG,OAAO,QAAQ,OAAO,GAAG;AACzC,UAAM,SAAS,SAAS,WAAW,KAAK,GAAG;AAC3C,QAAI,OAAO,QAAQ,MAAM;AACvB,aAAO,OAAO;AACd,gBAAU;AACV,iBAAW,OAAO;AAAA,IACpB;AAAA,EACF;AACA,QAAM,OAAO,KAAK,IAAI,QAAQ;AAC9B,QAAM,aAAa,YAAY,IAAI;AACnC,SAAO;AAAA,IACL,SAAU,UAAU,OAAQ;AAAA,IAC5B,gBAAgB,CAAC;AAAA,IACjB;AAAA,IACA,gBAAgB,UAAU,UAAU;AAAA,EACtC;AACF;AAEA,eAAsB,cACpB,KACA,KACA,OACA,YACA,QACA,WACqB;AACrB,MAAI,OAAO,QAAS,OAAM,IAAI,kBAAkB,WAAW,SAAS;AACpE,MAAI,IAAI,KAAK,OAAO,CAAC;AACrB,QAAM,OAAO,EAAE,OAAO,EAAE;AACxB,MAAI;AACF,UAAM,QAAQ,YAAY,KAAK,YAAY,QAAQ,IAAI;AACvD,UAAM,YAAY,YAAY,IAAI,WAAW,YAAY,QAAQ,IAAI;AACrE,UAAM,OAAO,MAAM,QAAQ,IAAI,CAAC,OAAO,SAAS,CAAC;AACjD,QAAI,OAAO,KAAK;AAChB,QAAI,QAAQ,EAAG,QAAO;AACtB,UAAM,UAAU,eAAe,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,MAAM,SAAS;AAChE,WAAO,UAAU,YAAY,KAAK,CAAC,GAAG,QAAQ,SAAS,IAAI,GAAG,QAAQ,KAAK;AAAA,EAC7E,UAAE;AACA,QAAI,IAAI,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,SAAS,eACP,KACA,WACA,YACA,WAC8C;AAC9C,MAAI,cAAc,OAAW,QAAO,EAAE,SAAS,IAAI;AACnD,QAAM,YAAY,UAAU;AAAA,IAC1B,WAAW,KAAK,UAAU;AAAA,IAC1B,WAAW,WAAW,UAAU;AAAA,EAClC;AACA,SAAO,EAAE,SAAS,aAAa,WAAW,GAAG,GAAG,OAAO,UAAU,SAAS,EAAE;AAC9E;AAEA,SAAS,UAAU,WAAiD;AAClE,MAAI,UAAU,UAAU,OAAW,QAAO;AAC1C,SAAO,UAAU,MAAM;AACzB;AAEA,SAAS,aAAa,OAAmB,UAAsC;AAC7E,QAAM,UAAU,UAAU,KAAK;AAC/B,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SAAO,aAAa,KAAK,OAAO;AAClC;AAEA,SAAS,WAAW,SAAuB,YAAgC;AACzE,QAAM,OAAO,IAAI,WAAW,QAAQ,SAAS,CAAC;AAC9C,QAAM,OAAO,IAAI,SAAS,KAAK,MAAM;AACrC,WAAS,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;AACtD,SAAK,WAAW,QAAQ,GAAG,QAAQ,KAAK,GAAa,IAAI;AAAA,EAC3D;AACA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,EAAE,UAAU,UAAU,YAAY,UAAU,EAAE;AAAA,IACtD,WAAW;AAAA,EACb;AACF;AAEA,SAAS,UAAU,QAAoB,OAA0C;AAC/E,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO;AAAA,IACL,SAAS,OAAO,MAAM,SAAS,OAAO,OAAO;AAAA,IAC7C,gBAAgB,OAAO,MAAM,gBAAgB,OAAO,cAAc;AAAA,IAClE,YAAY,OAAO;AAAA,IACnB,gBAAgB,OAAO;AAAA,EACzB;AACF;AAEA,SAAS,OAAO,OAAsB,UAA0B;AAC9D,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,QAAoB;AAC3B,SAAO,EAAE,SAAS,GAAG,gBAAgB,IAAI,YAAY,KAAK,gBAAgB,YAAY;AACxF;AAEA,SAAS,UAAU,YAAwC;AACzD,MAAI,cAAc,IAAK,QAAO;AAC9B,MAAI,cAAc,IAAK,QAAO;AAC9B,SAAO;AACT;AAEA,SAAS,SACP,WACA,KACA,KACiC;AACjC,MAAI,MAAM;AACV,MAAI,SAAS;AACb,QAAM,SAAS,KAAK,IAAI,UAAU,QAAQ,IAAI,SAAS,GAAG;AAC1D,WAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS,GAAG;AAC9C,UAAM,MAAM,UAAU,KAAK;AAC3B,UAAM,QAAQ,IAAI,QAAQ,GAAG;AAC7B,WAAO,MAAM;AACb,cAAU,MAAM;AAAA,EAClB;AACA,MAAI,UAAU,MAAO,QAAO,EAAE,OAAO,IAAI,MAAM,EAAE;AACjD,SAAO,EAAE,OAAO,KAAK,IAAI,GAAG,GAAG,MAAM,MAAM,OAAO;AACpD;AAEA,SAAS,YAAY,WAA2B;AAC9C,MAAI,OAAO;AACX,MAAI,QAAQ,MAAO,QAAO;AAC1B,SAAO,KAAK,KAAK,MAAM,IAAI;AAC7B;AAEA,gBAAgB,aACd,KACA,WACA,WACa;AACb,QAAM,OAAO,UAAU,OAAO,aAAa,EAAE;AAC7C,mBAAiB,SAAS,KAAK;AAC7B,UAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,QAAI,KAAK,MAAM;AACb,YAAM;AACN;AAAA,IACF;AACA,UAAM,UAAU,OAAO,OAAO,KAAK,KAAK;AAAA,EAC1C;AACF;AAEA,eAAe,YACb,QACA,YACA,QACA,MACuB;AACvB,QAAM,UAAoB,CAAC;AAC3B,MAAI,UAAU;AACd,mBAAiB,SAAS,QAAQ;AAChC,QAAI,OAAO,QAAS,OAAM,IAAI,kBAAkB,WAAW,SAAS;AACpE,QAAI,KAAK,UAAU,EAAG,MAAK,QAAQ,MAAM,OAAO;AAChD,eAAW,UAAU,UAAU,KAAK,EAAG,SAAQ,KAAK,MAAM;AAC1D,eAAW,aAAa,KAAK,IAAI;AACjC,QAAI,WAAW,WAAY;AAAA,EAC7B;AACA,SAAO,aAAa,KAAK,OAAO;AAClC;AAEA,SAAS,UAAU,OAA6B;AAC9C,MAAI,MAAM,OAAO,aAAa,QAAS,QAAO,aAAa,MAAM,IAAI;AACrE,MAAI,MAAM,OAAO,aAAa,SAAU,QAAO,cAAc,MAAM,IAAI;AACvE,SAAO,CAAC;AACV;AAEA,SAAS,aAAa,MAA4B;AAChD,QAAM,OAAO,IAAI,SAAS,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AACvE,QAAM,UAAoB,CAAC;AAC3B,QAAM,QAAQ,KAAK,MAAM,KAAK,aAAa,CAAC;AAC5C,WAAS,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG;AAC7C,YAAQ,KAAK,KAAK,SAAS,QAAQ,GAAG,IAAI,IAAI,KAAK;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,cAAc,MAA4B;AACjD,QAAM,OAAO,IAAI,SAAS,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AACvE,QAAM,UAAoB,CAAC;AAC3B,QAAM,QAAQ,KAAK,MAAM,KAAK,aAAa,CAAC;AAC5C,WAAS,QAAQ,GAAG,QAAQ,OAAO,SAAS,GAAG;AAC7C,YAAQ,KAAK,KAAK,WAAW,QAAQ,GAAG,IAAI,CAAC;AAAA,EAC/C;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport type { CaptionHistoryEntry } from './captions.ts';\nexport { captionsFromHistory } from './captions.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport type {
|
|
5
|
-
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;
|
|
4
|
+
"sourcesContent": ["export type {\n AudioChunk,\n AudioEncoding,\n AudioFormat,\n AudioSink,\n AudioSource,\n AudioTag,\n CharAlignment,\n TextSpan,\n} from './audio.ts';\nexport { resolveVoiceAuth } from './auth.ts';\nexport { blankUsage } from './blank-usage.ts';\nexport type { CaptionHistoryEntry } from './captions.ts';\nexport { captionsFromHistory } from './captions.ts';\nexport {\n convertAudio,\n convertAudioSource,\n decodeAudioBase64,\n encodeAudioBase64,\n} from './convert.ts';\nexport type {\n EchoCanceller,\n EchoControl,\n EchoMode,\n EchoReport,\n EchoStats,\n MicOpen,\n} from './echo.ts';\nexport {\n assertNodeEcho,\n calibrateEcho,\n chooseCapture,\n echoRuntime,\n measureEcho,\n micOpen,\n openEchoMic,\n} from './echo.ts';\nexport { VoiceRequestError } from './errors.ts';\nexport type { Mixer, OpenMixerOptions } from './mixer.ts';\nexport { openMixer } from './mixer.ts';\nexport type {\n Delivery,\n DeliverySupport,\n KnownRealtimeApi,\n KnownSpeechApi,\n KnownTranscriptionApi,\n RealtimeApi,\n RealtimeModel,\n SpeechApi,\n SpeechModel,\n TranscriptionApi,\n TranscriptionModel,\n VoiceInfo,\n VoiceModel,\n VoiceModelType,\n} from './models.ts';\nexport {\n getVoiceModel,\n listVoiceModels,\n registerVoiceModel,\n} from './models.ts';\nexport type {\n NodeAudioEngine,\n NodeAudioModule,\n NodeAudioOptions,\n NodeAudioSession,\n} from './node-host.ts';\nexport { openNodeAudio } from './node-host.ts';\nexport type {\n DeliveryPlan,\n RealtimeApiProvider,\n RealtimeContext,\n RealtimeEvent,\n RealtimeOptions,\n RealtimeSession,\n RealtimeSocket,\n RealtimeToken,\n TruncationPlan,\n} from './realtime.ts';\nexport { planDelivery, planTruncation, RealtimeLedger, realtimeSocket } from './realtime.ts';\nexport {\n connectRealtime,\n listen,\n registerRealtimeApiProvider,\n registerSpeechApiProvider,\n registerTranscriptionApiProvider,\n speak,\n transcribe,\n} from './registry.ts';\nexport type { AudioOut, BedChannel, Cue, CueSource, Soundscape } from './soundscape.ts';\nexport { followBed } from './soundscape.ts';\nexport type { SpeechApiProvider, SpeechEvent, SpeechInput } from './speech.ts';\nexport { SpeechEventStream } from './speech.ts';\nexport type { VoiceAgentEvent, VoiceMessageChange } from './spoken.ts';\nexport type {\n BindThinkerOptions,\n BoundThinker,\n Thinker,\n ThinkerCaption,\n ThinkerPhase,\n ThinkerUpdate,\n} from './talker.ts';\nexport { askAgent, bindThinker, talkerTools } from './talker.ts';\nexport type { RealtimeConstraints } from './tokens.ts';\nexport { mintRealtimeToken } from './tokens.ts';\nexport type {\n AudioInput,\n ListenOptions,\n Transcript,\n TranscriptionApiProvider,\n TranscriptionEvent,\n TranscriptSegment,\n} from './transcription.ts';\nexport { TranscriptionEventStream } from './transcription.ts';\nexport type {\n BargeIn,\n CaptionEvent,\n EchoRecommendation,\n VoiceConversation,\n VoiceCut,\n VoiceModels,\n VoicePhase,\n VoiceSession,\n VoiceTurnMeta,\n VoiceTurnOptions,\n} from './turns.ts';\nexport { voiceTurns } from './turns.ts';\nexport type {\n AudioCost,\n AudioCounters,\n AudioUsage,\n RealtimeAudioCost,\n SpeechAudioCost,\n TranscriptionAudioCost,\n} from './usage.ts';\nexport { addAudioCounters, priceAudio } from './usage.ts';\nexport type { VadHook, VadUpdate } from './vad.ts';\nexport { energyVad } from './vad.ts';\nexport { bindWorkletNode, WORKLET_RELEASE } from './worklet/release.ts';\nexport { AUDIO_TAP_PROCESSOR, audioTapWorkletUrl } from './worklet/tap.ts';\n"],
|
|
5
|
+
"mappings": "AAUA,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAE3B,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AASP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAElC,SAAS,iBAAiB;AAiB1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAOP,SAAS,qBAAqB;AAY9B,SAAS,cAAc,gBAAgB,gBAAgB,sBAAsB;AAC7E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,iBAAiB;AAE1B,SAAS,yBAAyB;AAUlC,SAAS,UAAU,aAAa,mBAAmB;AAEnD,SAAS,yBAAyB;AASlC,SAAS,gCAAgC;AAazC,SAAS,kBAAkB;AAS3B,SAAS,kBAAkB,kBAAkB;AAE7C,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB,uBAAuB;AACjD,SAAS,qBAAqB,0BAA0B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function replyHash(text: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UserMessage } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { VoiceConversation } from '../turns.ts';
|
|
3
|
+
export interface BrainBudget {
|
|
4
|
+
firstTokenMs: number;
|
|
5
|
+
hardDeadlineMs: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BrainOptions {
|
|
8
|
+
conversation(sessionKey: string, signal: AbortSignal): Promise<VoiceConversation>;
|
|
9
|
+
vendorTools: 'terminal' | 'drop';
|
|
10
|
+
budget: BrainBudget;
|
|
11
|
+
filler(user: UserMessage): string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Memory {
|
|
2
|
+
hash: string;
|
|
3
|
+
text: string;
|
|
4
|
+
user: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ParsedTurn {
|
|
7
|
+
model: string;
|
|
8
|
+
sessionKey: string;
|
|
9
|
+
tools: Set<string>;
|
|
10
|
+
prior?: string;
|
|
11
|
+
priorTools: boolean;
|
|
12
|
+
user: string;
|
|
13
|
+
}
|
|
14
|
+
export type TurnPlan = {
|
|
15
|
+
replay: true;
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
replay: false;
|
|
19
|
+
content: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function parseTurn(body: unknown): ParsedTurn | string;
|
|
22
|
+
export declare function turnPlan(memory: Memory | undefined, turn: ParsedTurn): TurnPlan;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BrainOptions } from './options.ts';
|
|
2
|
+
import type { Memory, ParsedTurn, TurnPlan } from './parse.ts';
|
|
3
|
+
type Controller = ReadableStreamDefaultController<Uint8Array>;
|
|
4
|
+
export declare function runTurn(options: BrainOptions, sessions: Map<string, Memory>, parsed: ParsedTurn, plan: TurnPlan, request: Request, controller: Controller): Promise<void>;
|
|
5
|
+
export {};
|
package/dist/types/echo.d.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { type AudioChunk, type AudioSource } from './audio.ts';
|
|
|
2
2
|
import type { AudioOut, Cue } from './soundscape.ts';
|
|
3
3
|
import type { EchoRecommendation } from './turns.ts';
|
|
4
4
|
export type EchoMode = 'browser' | 'wasm' | 'none';
|
|
5
|
+
export interface EchoStats {
|
|
6
|
+
delayMs: number | null;
|
|
7
|
+
cancellationDb: number | null;
|
|
8
|
+
}
|
|
5
9
|
export interface EchoCanceller {
|
|
6
10
|
cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk;
|
|
11
|
+
stats?(): EchoStats;
|
|
7
12
|
}
|
|
8
13
|
export interface EchoControl {
|
|
9
14
|
mode: EchoMode;
|
|
@@ -26,4 +31,4 @@ export declare function assertNodeEcho(mode: EchoMode, runtime: 'node' | 'browse
|
|
|
26
31
|
export declare function chooseCapture(tried: EchoMode, report: EchoReport): EchoMode;
|
|
27
32
|
export declare function openEchoMic(control: EchoControl, mic: AudioSource): AudioSource;
|
|
28
33
|
export declare function measureEcho(reference: Float32Array, mic: Float32Array, sampleRate: number): EchoReport;
|
|
29
|
-
export declare function calibrateEcho(out: AudioOut, mic: AudioSource, probe: Cue, durationMs: number, signal: AbortSignal): Promise<EchoReport>;
|
|
34
|
+
export declare function calibrateEcho(out: AudioOut, mic: AudioSource, probe: Cue, durationMs: number, signal: AbortSignal, canceller?: EchoCanceller): Promise<EchoReport>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { blankUsage } from './blank-usage.ts';
|
|
|
4
4
|
export type { CaptionHistoryEntry } from './captions.ts';
|
|
5
5
|
export { captionsFromHistory } from './captions.ts';
|
|
6
6
|
export { convertAudio, convertAudioSource, decodeAudioBase64, encodeAudioBase64, } from './convert.ts';
|
|
7
|
-
export type { EchoCanceller, EchoControl, EchoMode, EchoReport, MicOpen } from './echo.ts';
|
|
7
|
+
export type { EchoCanceller, EchoControl, EchoMode, EchoReport, EchoStats, MicOpen, } from './echo.ts';
|
|
8
8
|
export { assertNodeEcho, calibrateEcho, chooseCapture, echoRuntime, measureEcho, micOpen, openEchoMic, } from './echo.ts';
|
|
9
9
|
export { VoiceRequestError } from './errors.ts';
|
|
10
10
|
export type { Mixer, OpenMixerOptions } from './mixer.ts';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type WasmSource } from '@ai-ecoverse/wasm-webrtc-audio-processing';
|
|
2
|
+
import type { EchoCanceller, EchoStats } from '../echo.ts';
|
|
3
|
+
export interface WebRtcEchoOptions {
|
|
4
|
+
sampleRate: 16000 | 48000;
|
|
5
|
+
noiseSuppression?: boolean | 'low' | 'moderate' | 'high' | 'veryHigh';
|
|
6
|
+
vad?: boolean | 0 | 1 | 2 | 3;
|
|
7
|
+
wasm: WasmSource;
|
|
8
|
+
}
|
|
9
|
+
export interface WebRtcEcho extends EchoCanceller {
|
|
10
|
+
readonly sampleRate: 16000 | 48000;
|
|
11
|
+
readonly latencySamples: number;
|
|
12
|
+
stats(): EchoStats;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface NodeWebRtcModule {
|
|
16
|
+
wasmModule(): WebAssembly.Module;
|
|
17
|
+
}
|
|
18
|
+
export declare function createWebRtcEcho(options: WebRtcEchoOptions): WebRtcEcho;
|
|
19
|
+
export declare function createNodeWebRtcEcho(options: Omit<WebRtcEchoOptions, 'wasm'>, load?: () => Promise<NodeWebRtcModule>): Promise<WebRtcEcho>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { create } from "@ai-ecoverse/wasm-webrtc-audio-processing";
|
|
2
|
+
import { convertAudio } from "../convert.js";
|
|
3
|
+
import { VoiceRequestError } from "../errors.js";
|
|
4
|
+
function createWebRtcEcho(options) {
|
|
5
|
+
const aec = create({
|
|
6
|
+
wasm: options.wasm,
|
|
7
|
+
sampleRate: options.sampleRate,
|
|
8
|
+
noiseSuppression: options.noiseSuppression,
|
|
9
|
+
vad: options.vad
|
|
10
|
+
});
|
|
11
|
+
return adapt(aec, options.sampleRate);
|
|
12
|
+
}
|
|
13
|
+
async function createNodeWebRtcEcho(options, load = loadNodeWebRtc) {
|
|
14
|
+
let loaded;
|
|
15
|
+
try {
|
|
16
|
+
loaded = await load();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (missingModule(error)) {
|
|
19
|
+
throw new VoiceRequestError("error", "WebRTC echo canceller is unavailable");
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return createWebRtcEcho({ ...options, wasm: loaded.wasmModule() });
|
|
24
|
+
}
|
|
25
|
+
async function loadNodeWebRtc() {
|
|
26
|
+
return import("@ai-ecoverse/wasm-webrtc-audio-processing/node");
|
|
27
|
+
}
|
|
28
|
+
function missingModule(error) {
|
|
29
|
+
if (!(error instanceof Error)) return false;
|
|
30
|
+
if (!("code" in error)) return false;
|
|
31
|
+
return error.code === "ERR_MODULE_NOT_FOUND";
|
|
32
|
+
}
|
|
33
|
+
function adapt(aec, sampleRate) {
|
|
34
|
+
return {
|
|
35
|
+
sampleRate,
|
|
36
|
+
latencySamples: aec.latencySamples,
|
|
37
|
+
cancel(mic, reference) {
|
|
38
|
+
const cleaned = aec.cancel(floatsOf(mic, sampleRate), floatsOf(reference, sampleRate));
|
|
39
|
+
return pcmChunk(cleaned, mic, sampleRate);
|
|
40
|
+
},
|
|
41
|
+
stats() {
|
|
42
|
+
const live = aec.stats();
|
|
43
|
+
return { delayMs: live.delayMs, cancellationDb: live.erleDb };
|
|
44
|
+
},
|
|
45
|
+
destroy() {
|
|
46
|
+
aec.destroy();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function floatsOf(chunk, sampleRate) {
|
|
51
|
+
const pcm = convertAudio(chunk, { encoding: "pcmf32", sampleRate, channels: 1 });
|
|
52
|
+
const length = Math.floor(pcm.data.byteLength / 4);
|
|
53
|
+
const samples = new Float32Array(length);
|
|
54
|
+
const view = new DataView(pcm.data.buffer, pcm.data.byteOffset, pcm.data.byteLength);
|
|
55
|
+
for (let index = 0; index < length; index += 1) {
|
|
56
|
+
samples[index] = finiteSample(view.getFloat32(index * 4, true));
|
|
57
|
+
}
|
|
58
|
+
return samples;
|
|
59
|
+
}
|
|
60
|
+
function finiteSample(sample) {
|
|
61
|
+
if (Number.isFinite(sample)) return sample;
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
function pcmChunk(samples, mic, sampleRate) {
|
|
65
|
+
const data = new Uint8Array(samples.byteLength);
|
|
66
|
+
data.set(new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength));
|
|
67
|
+
return convertAudio(
|
|
68
|
+
{
|
|
69
|
+
data,
|
|
70
|
+
format: { encoding: "pcmf32", sampleRate, channels: 1 },
|
|
71
|
+
timestamp: mic.timestamp
|
|
72
|
+
},
|
|
73
|
+
mic.format
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
createNodeWebRtcEcho,
|
|
78
|
+
createWebRtcEcho
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/webrtc-aec/index.ts"],
|
|
4
|
+
"sourcesContent": ["import { create, type WasmSource } from '@ai-ecoverse/wasm-webrtc-audio-processing';\nimport type { AudioChunk } from '../audio.ts';\nimport { convertAudio } from '../convert.ts';\nimport type { EchoCanceller, EchoStats } from '../echo.ts';\nimport { VoiceRequestError } from '../errors.ts';\n\ntype Aec = ReturnType<typeof create>;\n\nexport interface WebRtcEchoOptions {\n sampleRate: 16000 | 48000;\n noiseSuppression?: boolean | 'low' | 'moderate' | 'high' | 'veryHigh';\n vad?: boolean | 0 | 1 | 2 | 3;\n wasm: WasmSource;\n}\n\nexport interface WebRtcEcho extends EchoCanceller {\n readonly sampleRate: 16000 | 48000;\n readonly latencySamples: number;\n stats(): EchoStats;\n destroy(): void;\n}\n\nexport interface NodeWebRtcModule {\n wasmModule(): WebAssembly.Module;\n}\n\nexport function createWebRtcEcho(options: WebRtcEchoOptions): WebRtcEcho {\n const aec = create({\n wasm: options.wasm,\n sampleRate: options.sampleRate,\n noiseSuppression: options.noiseSuppression,\n vad: options.vad,\n });\n return adapt(aec, options.sampleRate);\n}\n\nexport async function createNodeWebRtcEcho(\n options: Omit<WebRtcEchoOptions, 'wasm'>,\n load: () => Promise<NodeWebRtcModule> = loadNodeWebRtc\n): Promise<WebRtcEcho> {\n let loaded: NodeWebRtcModule;\n try {\n loaded = await load();\n } catch (error) {\n if (missingModule(error)) {\n throw new VoiceRequestError('error', 'WebRTC echo canceller is unavailable');\n }\n throw error;\n }\n return createWebRtcEcho({ ...options, wasm: loaded.wasmModule() });\n}\n\nasync function loadNodeWebRtc(): Promise<NodeWebRtcModule> {\n return import('@ai-ecoverse/wasm-webrtc-audio-processing/node');\n}\n\nfunction missingModule(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n if (!('code' in error)) return false;\n return error.code === 'ERR_MODULE_NOT_FOUND';\n}\n\nfunction adapt(aec: Aec, sampleRate: 16000 | 48000): WebRtcEcho {\n return {\n sampleRate,\n latencySamples: aec.latencySamples,\n cancel(mic: AudioChunk, reference: AudioChunk): AudioChunk {\n const cleaned = aec.cancel(floatsOf(mic, sampleRate), floatsOf(reference, sampleRate));\n return pcmChunk(cleaned, mic, sampleRate);\n },\n stats(): EchoStats {\n const live = aec.stats();\n return { delayMs: live.delayMs, cancellationDb: live.erleDb };\n },\n destroy(): void {\n aec.destroy();\n },\n };\n}\n\nfunction floatsOf(chunk: AudioChunk, sampleRate: 16000 | 48000): Float32Array {\n const pcm = convertAudio(chunk, { encoding: 'pcmf32', sampleRate, channels: 1 });\n const length = Math.floor(pcm.data.byteLength / 4);\n const samples = new Float32Array(length);\n const view = new DataView(pcm.data.buffer, pcm.data.byteOffset, pcm.data.byteLength);\n for (let index = 0; index < length; index += 1) {\n samples[index] = finiteSample(view.getFloat32(index * 4, true));\n }\n return samples;\n}\n\nfunction finiteSample(sample: number): number {\n if (Number.isFinite(sample)) return sample;\n return 0;\n}\n\nfunction pcmChunk(samples: Float32Array, mic: AudioChunk, sampleRate: 16000 | 48000): AudioChunk {\n const data = new Uint8Array(samples.byteLength);\n data.set(new Uint8Array(samples.buffer, samples.byteOffset, samples.byteLength));\n return convertAudio(\n {\n data,\n format: { encoding: 'pcmf32', sampleRate, channels: 1 },\n timestamp: mic.timestamp,\n },\n mic.format\n );\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,cAA+B;AAExC,SAAS,oBAAoB;AAE7B,SAAS,yBAAyB;AAsB3B,SAAS,iBAAiB,SAAwC;AACvE,QAAM,MAAM,OAAO;AAAA,IACjB,MAAM,QAAQ;AAAA,IACd,YAAY,QAAQ;AAAA,IACpB,kBAAkB,QAAQ;AAAA,IAC1B,KAAK,QAAQ;AAAA,EACf,CAAC;AACD,SAAO,MAAM,KAAK,QAAQ,UAAU;AACtC;AAEA,eAAsB,qBACpB,SACA,OAAwC,gBACnB;AACrB,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,KAAK;AAAA,EACtB,SAAS,OAAO;AACd,QAAI,cAAc,KAAK,GAAG;AACxB,YAAM,IAAI,kBAAkB,SAAS,sCAAsC;AAAA,IAC7E;AACA,UAAM;AAAA,EACR;AACA,SAAO,iBAAiB,EAAE,GAAG,SAAS,MAAM,OAAO,WAAW,EAAE,CAAC;AACnE;AAEA,eAAe,iBAA4C;AACzD,SAAO,OAAO,gDAAgD;AAChE;AAEA,SAAS,cAAc,OAAyB;AAC9C,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,MAAI,EAAE,UAAU,OAAQ,QAAO;AAC/B,SAAO,MAAM,SAAS;AACxB;AAEA,SAAS,MAAM,KAAU,YAAuC;AAC9D,SAAO;AAAA,IACL;AAAA,IACA,gBAAgB,IAAI;AAAA,IACpB,OAAO,KAAiB,WAAmC;AACzD,YAAM,UAAU,IAAI,OAAO,SAAS,KAAK,UAAU,GAAG,SAAS,WAAW,UAAU,CAAC;AACrF,aAAO,SAAS,SAAS,KAAK,UAAU;AAAA,IAC1C;AAAA,IACA,QAAmB;AACjB,YAAM,OAAO,IAAI,MAAM;AACvB,aAAO,EAAE,SAAS,KAAK,SAAS,gBAAgB,KAAK,OAAO;AAAA,IAC9D;AAAA,IACA,UAAgB;AACd,UAAI,QAAQ;AAAA,IACd;AAAA,EACF;AACF;AAEA,SAAS,SAAS,OAAmB,YAAyC;AAC5E,QAAM,MAAM,aAAa,OAAO,EAAE,UAAU,UAAU,YAAY,UAAU,EAAE,CAAC;AAC/E,QAAM,SAAS,KAAK,MAAM,IAAI,KAAK,aAAa,CAAC;AACjD,QAAM,UAAU,IAAI,aAAa,MAAM;AACvC,QAAM,OAAO,IAAI,SAAS,IAAI,KAAK,QAAQ,IAAI,KAAK,YAAY,IAAI,KAAK,UAAU;AACnF,WAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS,GAAG;AAC9C,YAAQ,KAAK,IAAI,aAAa,KAAK,WAAW,QAAQ,GAAG,IAAI,CAAC;AAAA,EAChE;AACA,SAAO;AACT;AAEA,SAAS,aAAa,QAAwB;AAC5C,MAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,SAAS,SAAuB,KAAiB,YAAuC;AAC/F,QAAM,OAAO,IAAI,WAAW,QAAQ,UAAU;AAC9C,OAAK,IAAI,IAAI,WAAW,QAAQ,QAAQ,QAAQ,YAAY,QAAQ,UAAU,CAAC;AAC/E,SAAO;AAAA,IACL;AAAA,MACE;AAAA,MACA,QAAQ,EAAE,UAAU,UAAU,YAAY,UAAU,EAAE;AAAA,MACtD,WAAW,IAAI;AAAA,IACjB;AAAA,IACA,IAAI;AAAA,EACN;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -85,6 +85,14 @@
|
|
|
85
85
|
"./fish-agents": {
|
|
86
86
|
"types": "./dist/types/fish-agents/index.d.ts",
|
|
87
87
|
"default": "./dist/fish-agents/index.js"
|
|
88
|
+
},
|
|
89
|
+
"./brain": {
|
|
90
|
+
"types": "./dist/types/brain/index.d.ts",
|
|
91
|
+
"default": "./dist/brain/index.js"
|
|
92
|
+
},
|
|
93
|
+
"./webrtc-aec": {
|
|
94
|
+
"types": "./dist/types/webrtc-aec/index.d.ts",
|
|
95
|
+
"default": "./dist/webrtc-aec/index.js"
|
|
88
96
|
}
|
|
89
97
|
},
|
|
90
98
|
"files": [
|
|
@@ -106,6 +114,7 @@
|
|
|
106
114
|
},
|
|
107
115
|
"devDependencies": {
|
|
108
116
|
"@ai-ecoverse/slicc-shared-web": "1.21.1",
|
|
117
|
+
"@ai-ecoverse/wasm-webrtc-audio-processing": "2.1.0-1",
|
|
109
118
|
"@biomejs/biome": "2.5.15",
|
|
110
119
|
"@earendil-works/pi-ai": "1.1.0",
|
|
111
120
|
"@earendil-works/pi-durable": "1.1.0",
|
|
@@ -119,12 +128,16 @@
|
|
|
119
128
|
"typescript": "7.0.2"
|
|
120
129
|
},
|
|
121
130
|
"peerDependencies": {
|
|
131
|
+
"@ai-ecoverse/wasm-webrtc-audio-processing": ">=2.1.0-1 <2.2.0",
|
|
122
132
|
"@earendil-works/pi-ai": "1.1.0",
|
|
123
133
|
"@earendil-works/pi-durable": "^1.1.0",
|
|
124
134
|
"node-web-audio-api": "^2.2.0",
|
|
125
135
|
"web-audio-api": "^1.6.0"
|
|
126
136
|
},
|
|
127
137
|
"peerDependenciesMeta": {
|
|
138
|
+
"@ai-ecoverse/wasm-webrtc-audio-processing": {
|
|
139
|
+
"optional": true
|
|
140
|
+
},
|
|
128
141
|
"@earendil-works/pi-durable": {
|
|
129
142
|
"optional": true
|
|
130
143
|
},
|