agents 0.22.0 → 0.23.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 +20 -1
- package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/browser/ai.js +4 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js.map +1 -1
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2325 -2019
- package/dist/chat/index.js +888 -518
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-BVVgDSdq.d.ts +1 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
- package/dist/index.d.ts +91 -82
- package/dist/index.js +3 -2
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +35 -17
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +6 -13
- package/docs/context.md +131 -0
- package/docs/index.md +15 -12
- package/docs/lifecycle.md +102 -55
- package/docs/long-running-agents.md +2 -2
- package/docs/mcp-servers.md +5 -1
- package/docs/resumable-streaming.md +1 -1
- package/docs/routing.md +105 -0
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +184 -124
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +115 -13
- package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/diagnostics-CaBjfz4J.js.map +0 -1
- package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
- package/dist/experimental/memory/session/index.d.ts +0 -671
- package/dist/experimental/memory/session/index.js +0 -2379
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/scheduler-CR9RHGos.js +0 -857
- package/dist/scheduler-CR9RHGos.js.map +0 -1
- package/dist/src-5W6JNKVb.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { VoiceClient, WebSocketVoiceTransport } from "./client.js";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
//#region src/voice/react.tsx
|
|
4
|
+
/**
|
|
5
|
+
* React hook for voice-to-text input. Captures microphone audio, streams it
|
|
6
|
+
* to a server-side VoiceAgent for STT, and returns the transcript as a string.
|
|
7
|
+
*
|
|
8
|
+
* Unlike `useVoiceAgent`, this hook is optimised for dictation — it accumulates
|
|
9
|
+
* user transcripts into a single string and ignores assistant responses / TTS.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const { transcript, interimTranscript, isListening, start, stop } = useVoiceInput({
|
|
14
|
+
* agent: "voice-input-agent"
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* <textarea value={transcript + (interimTranscript ? " " + interimTranscript : "")} />
|
|
18
|
+
* <button onClick={isListening ? stop : start}>
|
|
19
|
+
* {isListening ? "Stop" : "Dictate"}
|
|
20
|
+
* </button>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function useVoiceInput(options) {
|
|
24
|
+
const connectionKey = useMemo(() => `${options.agent}:${options.name ?? "default"}:${options.host ?? ""}:${options.silenceThreshold ?? ""}:${options.silenceDurationMs ?? ""}`, [
|
|
25
|
+
options.agent,
|
|
26
|
+
options.name,
|
|
27
|
+
options.host,
|
|
28
|
+
options.silenceThreshold,
|
|
29
|
+
options.silenceDurationMs
|
|
30
|
+
]);
|
|
31
|
+
const clientRef = useRef(null);
|
|
32
|
+
const [transcript, setTranscript] = useState("");
|
|
33
|
+
const [interimTranscript, setInterimTranscript] = useState(null);
|
|
34
|
+
const [isListening, setIsListening] = useState(false);
|
|
35
|
+
const [turnMetrics, setTurnMetrics] = useState(null);
|
|
36
|
+
const [audioLevel, setAudioLevel] = useState(0);
|
|
37
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setIsListening(false);
|
|
41
|
+
setInterimTranscript(null);
|
|
42
|
+
setTurnMetrics(null);
|
|
43
|
+
setAudioLevel(0);
|
|
44
|
+
setIsMuted(false);
|
|
45
|
+
setError(null);
|
|
46
|
+
const client = new VoiceClient({
|
|
47
|
+
agent: options.agent,
|
|
48
|
+
name: options.name,
|
|
49
|
+
host: options.host,
|
|
50
|
+
silenceThreshold: options.silenceThreshold,
|
|
51
|
+
silenceDurationMs: options.silenceDurationMs
|
|
52
|
+
});
|
|
53
|
+
clientRef.current = client;
|
|
54
|
+
client.connect();
|
|
55
|
+
const onTranscript = () => {
|
|
56
|
+
setTranscript(client.transcript.filter((m) => m.role === "user").map((m) => m.text).join(" "));
|
|
57
|
+
};
|
|
58
|
+
const onInterim = () => setInterimTranscript(client.interimTranscript);
|
|
59
|
+
const onTurnMetrics = (value) => setTurnMetrics(value);
|
|
60
|
+
const onAudioLevel = () => setAudioLevel(client.audioLevel);
|
|
61
|
+
const onMute = () => setIsMuted(client.isMuted);
|
|
62
|
+
const onError = () => setError(client.error);
|
|
63
|
+
const onStatus = () => {
|
|
64
|
+
const s = client.status;
|
|
65
|
+
setIsListening(s === "listening" || s === "thinking");
|
|
66
|
+
};
|
|
67
|
+
client.addEventListener("transcriptchange", onTranscript);
|
|
68
|
+
client.addEventListener("interimtranscript", onInterim);
|
|
69
|
+
client.addEventListener("turnmetrics", onTurnMetrics);
|
|
70
|
+
client.addEventListener("audiolevelchange", onAudioLevel);
|
|
71
|
+
client.addEventListener("mutechange", onMute);
|
|
72
|
+
client.addEventListener("error", onError);
|
|
73
|
+
client.addEventListener("statuschange", onStatus);
|
|
74
|
+
return () => {
|
|
75
|
+
client.removeEventListener("transcriptchange", onTranscript);
|
|
76
|
+
client.removeEventListener("interimtranscript", onInterim);
|
|
77
|
+
client.removeEventListener("turnmetrics", onTurnMetrics);
|
|
78
|
+
client.removeEventListener("audiolevelchange", onAudioLevel);
|
|
79
|
+
client.removeEventListener("mutechange", onMute);
|
|
80
|
+
client.removeEventListener("error", onError);
|
|
81
|
+
client.removeEventListener("statuschange", onStatus);
|
|
82
|
+
client.disconnect();
|
|
83
|
+
};
|
|
84
|
+
}, [connectionKey]);
|
|
85
|
+
return {
|
|
86
|
+
transcript,
|
|
87
|
+
interimTranscript,
|
|
88
|
+
turnMetrics,
|
|
89
|
+
isListening,
|
|
90
|
+
audioLevel,
|
|
91
|
+
isMuted,
|
|
92
|
+
error,
|
|
93
|
+
start: useCallback(() => clientRef.current.startCall(), []),
|
|
94
|
+
stop: useCallback(() => clientRef.current.endCall(), []),
|
|
95
|
+
toggleMute: useCallback(() => clientRef.current.toggleMute(), []),
|
|
96
|
+
clear: useCallback(() => setTranscript(""), [])
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* React hook that wraps VoiceClient, syncing its state into React state.
|
|
101
|
+
* All audio infrastructure (mic capture, playback, silence/interrupt detection,
|
|
102
|
+
* voice protocol) is handled by VoiceClient — this hook just bridges to React.
|
|
103
|
+
*
|
|
104
|
+
* When the connection identity changes (agent, name, or host), the hook
|
|
105
|
+
* automatically disconnects the old client, creates a new one, and reconnects.
|
|
106
|
+
* The `onReconnect` callback fires when this happens.
|
|
107
|
+
*/
|
|
108
|
+
function useVoiceAgent(options) {
|
|
109
|
+
const queryString = useMemo(() => options.query ? JSON.stringify(options.query) : "", [options.query]);
|
|
110
|
+
const enabled = options.enabled ?? true;
|
|
111
|
+
const outputDeviceId = options.outputDeviceId;
|
|
112
|
+
const connectionKey = useMemo(() => `${options.agent}:${options.name ?? "default"}:${options.host ?? ""}:${options.silenceThreshold ?? ""}:${options.silenceDurationMs ?? ""}:${options.interruptThreshold ?? ""}:${options.interruptChunks ?? ""}:${queryString}`, [
|
|
113
|
+
options.agent,
|
|
114
|
+
options.name,
|
|
115
|
+
options.host,
|
|
116
|
+
options.silenceThreshold,
|
|
117
|
+
options.silenceDurationMs,
|
|
118
|
+
options.interruptThreshold,
|
|
119
|
+
options.interruptChunks,
|
|
120
|
+
queryString
|
|
121
|
+
]);
|
|
122
|
+
const effectKey = useMemo(() => `${enabled ? "enabled" : "disabled"}:${connectionKey}`, [enabled, connectionKey]);
|
|
123
|
+
const clientRef = useRef(null);
|
|
124
|
+
const activeKeyRef = useRef(null);
|
|
125
|
+
const onReconnectRef = useRef(options.onReconnect);
|
|
126
|
+
onReconnectRef.current = options.onReconnect;
|
|
127
|
+
const [status, setStatus] = useState("idle");
|
|
128
|
+
const [transcript, setTranscript] = useState([]);
|
|
129
|
+
const [metrics, setMetrics] = useState(null);
|
|
130
|
+
const [turnMetrics, setTurnMetrics] = useState(null);
|
|
131
|
+
const [audioLevel, setAudioLevel] = useState(0);
|
|
132
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
133
|
+
const [connected, setConnected] = useState(false);
|
|
134
|
+
const [error, setError] = useState(null);
|
|
135
|
+
const [outputDeviceError, setOutputDeviceError] = useState(null);
|
|
136
|
+
const [interimTranscript, setInterimTranscript] = useState(null);
|
|
137
|
+
const [lastCustomMessage, setLastCustomMessage] = useState(null);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
setStatus("idle");
|
|
140
|
+
setTranscript([]);
|
|
141
|
+
setMetrics(null);
|
|
142
|
+
setTurnMetrics(null);
|
|
143
|
+
setAudioLevel(0);
|
|
144
|
+
setIsMuted(false);
|
|
145
|
+
setConnected(false);
|
|
146
|
+
setError(null);
|
|
147
|
+
setOutputDeviceError(null);
|
|
148
|
+
setInterimTranscript(null);
|
|
149
|
+
setLastCustomMessage(null);
|
|
150
|
+
if (!enabled) {
|
|
151
|
+
clientRef.current = null;
|
|
152
|
+
activeKeyRef.current = null;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const isReconnect = activeKeyRef.current !== null && activeKeyRef.current !== connectionKey;
|
|
156
|
+
activeKeyRef.current = connectionKey;
|
|
157
|
+
if (isReconnect) onReconnectRef.current?.();
|
|
158
|
+
const { enabled: _enabled, onReconnect: _onReconnect, ...clientOptions } = options;
|
|
159
|
+
const client = new VoiceClient(clientOptions);
|
|
160
|
+
clientRef.current = client;
|
|
161
|
+
client.connect();
|
|
162
|
+
const onStatus = (s) => setStatus(s);
|
|
163
|
+
const onTranscript = (t) => setTranscript(t);
|
|
164
|
+
const onMetrics = (m) => setMetrics(m);
|
|
165
|
+
const onTurnMetrics = (value) => setTurnMetrics(value);
|
|
166
|
+
const onAudioLevel = (level) => setAudioLevel(level);
|
|
167
|
+
const onMute = (muted) => setIsMuted(muted);
|
|
168
|
+
const onConnection = (c) => setConnected(c);
|
|
169
|
+
const onError = (e) => setError(e);
|
|
170
|
+
const onOutputDeviceError = (e) => setOutputDeviceError(e);
|
|
171
|
+
const onInterim = (text) => setInterimTranscript(text);
|
|
172
|
+
client.addEventListener("statuschange", onStatus);
|
|
173
|
+
client.addEventListener("transcriptchange", onTranscript);
|
|
174
|
+
client.addEventListener("interimtranscript", onInterim);
|
|
175
|
+
client.addEventListener("metricschange", onMetrics);
|
|
176
|
+
client.addEventListener("turnmetrics", onTurnMetrics);
|
|
177
|
+
client.addEventListener("audiolevelchange", onAudioLevel);
|
|
178
|
+
client.addEventListener("mutechange", onMute);
|
|
179
|
+
client.addEventListener("connectionchange", onConnection);
|
|
180
|
+
client.addEventListener("error", onError);
|
|
181
|
+
client.addEventListener("outputdeviceerror", onOutputDeviceError);
|
|
182
|
+
return () => {
|
|
183
|
+
client.removeEventListener("statuschange", onStatus);
|
|
184
|
+
client.removeEventListener("transcriptchange", onTranscript);
|
|
185
|
+
client.removeEventListener("interimtranscript", onInterim);
|
|
186
|
+
client.removeEventListener("metricschange", onMetrics);
|
|
187
|
+
client.removeEventListener("turnmetrics", onTurnMetrics);
|
|
188
|
+
client.removeEventListener("audiolevelchange", onAudioLevel);
|
|
189
|
+
client.removeEventListener("mutechange", onMute);
|
|
190
|
+
client.removeEventListener("connectionchange", onConnection);
|
|
191
|
+
client.removeEventListener("error", onError);
|
|
192
|
+
client.removeEventListener("outputdeviceerror", onOutputDeviceError);
|
|
193
|
+
if (clientRef.current === client) clientRef.current = null;
|
|
194
|
+
client.disconnect();
|
|
195
|
+
};
|
|
196
|
+
}, [effectKey]);
|
|
197
|
+
const startCall = useCallback(() => clientRef.current?.startCall() ?? Promise.resolve(), []);
|
|
198
|
+
const endCall = useCallback(() => clientRef.current?.endCall(), []);
|
|
199
|
+
const toggleMute = useCallback(() => clientRef.current?.toggleMute(), []);
|
|
200
|
+
const sendText = useCallback((text) => clientRef.current?.sendText(text), []);
|
|
201
|
+
const sendJSON = useCallback((data) => clientRef.current?.sendJSON(data), []);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const client = clientRef.current;
|
|
204
|
+
if (!client) return;
|
|
205
|
+
const onCustom = (msg) => setLastCustomMessage(msg);
|
|
206
|
+
client.addEventListener("custommessage", onCustom);
|
|
207
|
+
return () => client.removeEventListener("custommessage", onCustom);
|
|
208
|
+
}, [effectKey]);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
clientRef.current?.setOutputDevice(outputDeviceId);
|
|
211
|
+
}, [effectKey, outputDeviceId]);
|
|
212
|
+
return {
|
|
213
|
+
status,
|
|
214
|
+
transcript,
|
|
215
|
+
interimTranscript,
|
|
216
|
+
metrics,
|
|
217
|
+
turnMetrics,
|
|
218
|
+
audioLevel,
|
|
219
|
+
isMuted,
|
|
220
|
+
connected,
|
|
221
|
+
error,
|
|
222
|
+
outputDeviceError,
|
|
223
|
+
startCall,
|
|
224
|
+
endCall,
|
|
225
|
+
toggleMute,
|
|
226
|
+
sendText,
|
|
227
|
+
sendJSON,
|
|
228
|
+
lastCustomMessage
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
export { WebSocketVoiceTransport, useVoiceAgent, useVoiceInput };
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","names":[],"sources":["../../src/voice/react.tsx"],"sourcesContent":["import { useEffect, useRef, useState, useCallback, useMemo } from \"react\";\nimport {\n VoiceClient,\n type VoiceClientOptions,\n type VoiceStatus,\n type TranscriptMessage,\n type VoicePipelineMetrics,\n type VoiceTurnMetrics\n} from \"./client\";\n\n// Re-export types so consumers can import everything from agents/voice/react\nexport type {\n VoiceStatus,\n VoiceRole,\n VoiceAudioFormat,\n VoiceAudioInput,\n VoiceTransport,\n TranscriptMessage,\n VoicePipelineMetrics,\n VoiceTurnMetrics,\n VoiceTurnOutcome,\n VoiceTurnSource,\n VoiceClientOptions,\n VoiceClientEvent,\n VoiceClientEventMap,\n VoiceConnectionDiagnostic,\n VoiceTransportCloseInfo,\n VoiceError,\n VoiceErrorCode,\n VoiceErrorStage\n} from \"./client\";\nexport { WebSocketVoiceTransport } from \"./client\";\n\n/** Options accepted by useVoiceAgent. */\nexport interface UseVoiceAgentOptions extends VoiceClientOptions {\n /**\n * Whether the hook should create and connect a VoiceClient.\n *\n * @default true\n */\n enabled?: boolean;\n /**\n * Called when the hook reconnects due to option changes (e.g., agent name\n * or instance name changed). Use this to show a toast or notification.\n */\n onReconnect?: () => void;\n}\n\nexport interface UseVoiceAgentReturn {\n status: VoiceStatus;\n transcript: TranscriptMessage[];\n /**\n * The current interim (partial) transcript from streaming STT.\n * Updates in real time as the user speaks. null when not available.\n */\n interimTranscript: string | null;\n metrics: VoicePipelineMetrics | null;\n /** Last stable terminal summary for a speech or text turn. */\n turnMetrics: VoiceTurnMetrics | null;\n audioLevel: number;\n isMuted: boolean;\n connected: boolean;\n error: string | null;\n outputDeviceError: string | null;\n startCall: () => Promise<void>;\n endCall: () => void;\n toggleMute: () => void;\n sendText: (text: string) => void;\n /** Send arbitrary JSON to the agent (app-level messages). */\n sendJSON: (data: Record<string, unknown>) => void;\n /** The last non-voice-protocol message received from the server. */\n lastCustomMessage: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// useVoiceInput — lightweight hook for voice-to-text dictation\n// ---------------------------------------------------------------------------\n\n/** Options accepted by useVoiceInput. */\nexport interface UseVoiceInputOptions {\n /** Agent name (matches the server-side Durable Object class). */\n agent: string;\n /** Instance name for the agent. @default \"default\" */\n name?: string;\n /** Host to connect to. @default window.location.host */\n host?: string;\n\n /** RMS threshold below which audio is considered silence. @default 0.04 */\n silenceThreshold?: number;\n /** How long silence must last before sending end_of_speech (ms). @default 500 */\n silenceDurationMs?: number;\n}\n\nexport interface UseVoiceInputReturn {\n /** Accumulated final transcript text from all utterances. */\n transcript: string;\n /**\n * Current interim (partial) transcript from streaming STT.\n * Updates in real time as the user speaks. null when not available.\n */\n interimTranscript: string | null;\n /** Last stable terminal summary for a speech turn. */\n turnMetrics: VoiceTurnMetrics | null;\n /** Whether the mic is actively listening. */\n isListening: boolean;\n /** Current audio level (0–1) for visual feedback (e.g. waveform). */\n audioLevel: number;\n /** Whether the mic is muted. */\n isMuted: boolean;\n /** Any error message. */\n error: string | null;\n /** Start listening — requests mic permission and begins streaming audio. */\n start: () => Promise<void>;\n /** Stop listening — releases the mic. */\n stop: () => void;\n /** Toggle mute (mic stays open but audio is not sent). */\n toggleMute: () => void;\n /** Clear the accumulated transcript. */\n clear: () => void;\n}\n\n/**\n * React hook for voice-to-text input. Captures microphone audio, streams it\n * to a server-side VoiceAgent for STT, and returns the transcript as a string.\n *\n * Unlike `useVoiceAgent`, this hook is optimised for dictation — it accumulates\n * user transcripts into a single string and ignores assistant responses / TTS.\n *\n * @example\n * ```tsx\n * const { transcript, interimTranscript, isListening, start, stop } = useVoiceInput({\n * agent: \"voice-input-agent\"\n * });\n *\n * <textarea value={transcript + (interimTranscript ? \" \" + interimTranscript : \"\")} />\n * <button onClick={isListening ? stop : start}>\n * {isListening ? \"Stop\" : \"Dictate\"}\n * </button>\n * ```\n */\nexport function useVoiceInput(\n options: UseVoiceInputOptions\n): UseVoiceInputReturn {\n const connectionKey = useMemo(\n () =>\n `${options.agent}:${options.name ?? \"default\"}:${options.host ?? \"\"}:${options.silenceThreshold ?? \"\"}:${options.silenceDurationMs ?? \"\"}`,\n [\n options.agent,\n options.name,\n options.host,\n options.silenceThreshold,\n options.silenceDurationMs\n ]\n );\n\n const clientRef = useRef<VoiceClient | null>(null);\n\n const [transcript, setTranscript] = useState(\"\");\n const [interimTranscript, setInterimTranscript] = useState<string | null>(\n null\n );\n const [isListening, setIsListening] = useState(false);\n const [turnMetrics, setTurnMetrics] = useState<VoiceTurnMetrics | null>(null);\n const [audioLevel, setAudioLevel] = useState(0);\n const [isMuted, setIsMuted] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Connect on mount or when connection identity changes\n useEffect(() => {\n setIsListening(false);\n setInterimTranscript(null);\n setTurnMetrics(null);\n setAudioLevel(0);\n setIsMuted(false);\n setError(null);\n\n const client = new VoiceClient({\n agent: options.agent,\n name: options.name,\n host: options.host,\n silenceThreshold: options.silenceThreshold,\n silenceDurationMs: options.silenceDurationMs\n });\n clientRef.current = client;\n client.connect();\n\n // Sync user transcripts into a single accumulated string\n const onTranscript = () => {\n const msgs = client.transcript;\n const userTexts = msgs\n .filter((m) => m.role === \"user\")\n .map((m) => m.text);\n setTranscript(userTexts.join(\" \"));\n };\n\n const onInterim = () => setInterimTranscript(client.interimTranscript);\n const onTurnMetrics = (value: VoiceTurnMetrics) => setTurnMetrics(value);\n const onAudioLevel = () => setAudioLevel(client.audioLevel);\n const onMute = () => setIsMuted(client.isMuted);\n const onError = () => setError(client.error);\n\n const onStatus = () => {\n const s = client.status;\n setIsListening(s === \"listening\" || s === \"thinking\");\n };\n\n client.addEventListener(\"transcriptchange\", onTranscript);\n client.addEventListener(\"interimtranscript\", onInterim);\n client.addEventListener(\"turnmetrics\", onTurnMetrics);\n client.addEventListener(\"audiolevelchange\", onAudioLevel);\n client.addEventListener(\"mutechange\", onMute);\n client.addEventListener(\"error\", onError);\n client.addEventListener(\"statuschange\", onStatus);\n\n return () => {\n client.removeEventListener(\"transcriptchange\", onTranscript);\n client.removeEventListener(\"interimtranscript\", onInterim);\n client.removeEventListener(\"turnmetrics\", onTurnMetrics);\n client.removeEventListener(\"audiolevelchange\", onAudioLevel);\n client.removeEventListener(\"mutechange\", onMute);\n client.removeEventListener(\"error\", onError);\n client.removeEventListener(\"statuschange\", onStatus);\n client.disconnect();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- reconnect when connection identity changes\n }, [connectionKey]);\n\n const start = useCallback(() => clientRef.current!.startCall(), []);\n const stop = useCallback(() => clientRef.current!.endCall(), []);\n const toggleMute = useCallback(() => clientRef.current!.toggleMute(), []);\n const clear = useCallback(() => setTranscript(\"\"), []);\n\n return {\n transcript,\n interimTranscript,\n turnMetrics,\n isListening,\n audioLevel,\n isMuted,\n error,\n start,\n stop,\n toggleMute,\n clear\n };\n}\n\n// ---------------------------------------------------------------------------\n// useVoiceAgent — full-featured hook for conversational voice agents\n// ---------------------------------------------------------------------------\n\n/**\n * React hook that wraps VoiceClient, syncing its state into React state.\n * All audio infrastructure (mic capture, playback, silence/interrupt detection,\n * voice protocol) is handled by VoiceClient — this hook just bridges to React.\n *\n * When the connection identity changes (agent, name, or host), the hook\n * automatically disconnects the old client, creates a new one, and reconnects.\n * The `onReconnect` callback fires when this happens.\n */\nexport function useVoiceAgent(\n options: UseVoiceAgentOptions\n): UseVoiceAgentReturn {\n // Derive a stable key from the connection-identity fields.\n // When this changes, we tear down the old client and create a new one.\n const queryString = useMemo(\n () => (options.query ? JSON.stringify(options.query) : \"\"),\n [options.query]\n );\n\n const enabled = options.enabled ?? true;\n const outputDeviceId = options.outputDeviceId;\n\n const connectionKey = useMemo(\n () =>\n `${options.agent}:${options.name ?? \"default\"}:${options.host ?? \"\"}:${options.silenceThreshold ?? \"\"}:${options.silenceDurationMs ?? \"\"}:${options.interruptThreshold ?? \"\"}:${options.interruptChunks ?? \"\"}:${queryString}`,\n [\n options.agent,\n options.name,\n options.host,\n options.silenceThreshold,\n options.silenceDurationMs,\n options.interruptThreshold,\n options.interruptChunks,\n queryString\n ]\n );\n\n const effectKey = useMemo(\n () => `${enabled ? \"enabled\" : \"disabled\"}:${connectionKey}`,\n [enabled, connectionKey]\n );\n\n const clientRef = useRef<VoiceClient | null>(null);\n const activeKeyRef = useRef<string | null>(null);\n const onReconnectRef = useRef(options.onReconnect);\n onReconnectRef.current = options.onReconnect;\n\n // React state mirrors VoiceClient state\n const [status, setStatus] = useState<VoiceStatus>(\"idle\");\n const [transcript, setTranscript] = useState<TranscriptMessage[]>([]);\n const [metrics, setMetrics] = useState<VoicePipelineMetrics | null>(null);\n const [turnMetrics, setTurnMetrics] = useState<VoiceTurnMetrics | null>(null);\n const [audioLevel, setAudioLevel] = useState(0);\n const [isMuted, setIsMuted] = useState(false);\n const [connected, setConnected] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [outputDeviceError, setOutputDeviceError] = useState<string | null>(\n null\n );\n const [interimTranscript, setInterimTranscript] = useState<string | null>(\n null\n );\n const [lastCustomMessage, setLastCustomMessage] = useState<unknown>(null);\n\n // Connect on mount or when connection identity changes\n useEffect(() => {\n // Reset state for a fresh or disabled connection\n setStatus(\"idle\");\n setTranscript([]);\n setMetrics(null);\n setTurnMetrics(null);\n setAudioLevel(0);\n setIsMuted(false);\n setConnected(false);\n setError(null);\n setOutputDeviceError(null);\n setInterimTranscript(null);\n setLastCustomMessage(null);\n\n if (!enabled) {\n clientRef.current = null;\n activeKeyRef.current = null;\n return;\n }\n\n const isReconnect =\n activeKeyRef.current !== null && activeKeyRef.current !== connectionKey;\n activeKeyRef.current = connectionKey;\n\n // Fire reconnect callback (e.g., to show a toast)\n if (isReconnect) {\n onReconnectRef.current?.();\n }\n\n const {\n enabled: _enabled,\n onReconnect: _onReconnect,\n ...clientOptions\n } = options;\n const client = new VoiceClient(clientOptions);\n clientRef.current = client;\n client.connect();\n\n // Sync handlers — read state from client and push to React\n const onStatus = (s: VoiceStatus) => setStatus(s);\n const onTranscript = (t: TranscriptMessage[]) => setTranscript(t);\n const onMetrics = (m: VoicePipelineMetrics | null) => setMetrics(m);\n const onTurnMetrics = (value: VoiceTurnMetrics) => setTurnMetrics(value);\n const onAudioLevel = (level: number) => setAudioLevel(level);\n const onMute = (muted: boolean) => setIsMuted(muted);\n const onConnection = (c: boolean) => setConnected(c);\n const onError = (e: string | null) => setError(e);\n const onOutputDeviceError = (e: string | null) => setOutputDeviceError(e);\n const onInterim = (text: string | null) => setInterimTranscript(text);\n\n client.addEventListener(\"statuschange\", onStatus);\n client.addEventListener(\"transcriptchange\", onTranscript);\n client.addEventListener(\"interimtranscript\", onInterim);\n client.addEventListener(\"metricschange\", onMetrics);\n client.addEventListener(\"turnmetrics\", onTurnMetrics);\n client.addEventListener(\"audiolevelchange\", onAudioLevel);\n client.addEventListener(\"mutechange\", onMute);\n client.addEventListener(\"connectionchange\", onConnection);\n client.addEventListener(\"error\", onError);\n client.addEventListener(\"outputdeviceerror\", onOutputDeviceError);\n\n return () => {\n client.removeEventListener(\"statuschange\", onStatus);\n client.removeEventListener(\"transcriptchange\", onTranscript);\n client.removeEventListener(\"interimtranscript\", onInterim);\n client.removeEventListener(\"metricschange\", onMetrics);\n client.removeEventListener(\"turnmetrics\", onTurnMetrics);\n client.removeEventListener(\"audiolevelchange\", onAudioLevel);\n client.removeEventListener(\"mutechange\", onMute);\n client.removeEventListener(\"connectionchange\", onConnection);\n client.removeEventListener(\"error\", onError);\n client.removeEventListener(\"outputdeviceerror\", onOutputDeviceError);\n if (clientRef.current === client) {\n clientRef.current = null;\n }\n client.disconnect();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- reconnect when connection identity changes\n }, [effectKey]);\n\n // Stable action callbacks — always use the latest client\n const startCall = useCallback(\n () => clientRef.current?.startCall() ?? Promise.resolve(),\n []\n );\n const endCall = useCallback(() => clientRef.current?.endCall(), []);\n const toggleMute = useCallback(() => clientRef.current?.toggleMute(), []);\n const sendText = useCallback(\n (text: string) => clientRef.current?.sendText(text),\n []\n );\n const sendJSON = useCallback(\n (data: Record<string, unknown>) => clientRef.current?.sendJSON(data),\n []\n );\n\n // Listen for custom messages — needs a separate effect since it must\n // attach to the latest client.\n useEffect(() => {\n const client = clientRef.current;\n if (!client) return;\n const onCustom = (msg: unknown) => setLastCustomMessage(msg);\n client.addEventListener(\"custommessage\", onCustom);\n return () => client.removeEventListener(\"custommessage\", onCustom);\n }, [effectKey]);\n\n useEffect(() => {\n void clientRef.current?.setOutputDevice(outputDeviceId);\n }, [effectKey, outputDeviceId]);\n\n return {\n status,\n transcript,\n interimTranscript,\n metrics,\n turnMetrics,\n audioLevel,\n isMuted,\n connected,\n error,\n outputDeviceError,\n startCall,\n endCall,\n toggleMute,\n sendText,\n sendJSON,\n lastCustomMessage\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/voice/sfu.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pure utility functions for the Cloudflare Realtime SFU integration.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from sfu.ts for testability. These handle:
|
|
6
|
+
* - Protobuf varint encoding/decoding
|
|
7
|
+
* - SFU WebSocket adapter protobuf packet encoding/decoding
|
|
8
|
+
* - Audio format conversion (48kHz stereo ↔ 16kHz mono)
|
|
9
|
+
*/
|
|
10
|
+
declare function decodeVarint(
|
|
11
|
+
buf: Uint8Array,
|
|
12
|
+
offset: number
|
|
13
|
+
): {
|
|
14
|
+
value: number;
|
|
15
|
+
bytesRead: number;
|
|
16
|
+
};
|
|
17
|
+
declare function encodeVarint(value: number): Uint8Array;
|
|
18
|
+
/** Extract the PCM payload from a protobuf Packet message. */
|
|
19
|
+
declare function extractPayloadFromProtobuf(
|
|
20
|
+
data: ArrayBuffer
|
|
21
|
+
): Uint8Array | null;
|
|
22
|
+
/** Encode PCM payload into a protobuf Packet message (for ingest/buffer mode — just payload). */
|
|
23
|
+
declare function encodePayloadToProtobuf(payload: Uint8Array): ArrayBuffer;
|
|
24
|
+
/** Downsample 48kHz stereo interleaved PCM to 16kHz mono PCM (both 16-bit LE). */
|
|
25
|
+
declare function downsample48kStereoTo16kMono(
|
|
26
|
+
stereo48k: Uint8Array
|
|
27
|
+
): ArrayBuffer;
|
|
28
|
+
/** Upsample 16kHz mono PCM to 48kHz stereo interleaved PCM (both 16-bit LE). */
|
|
29
|
+
declare function upsample16kMonoTo48kStereo(mono16k: ArrayBuffer): Uint8Array;
|
|
30
|
+
interface SFUConfig {
|
|
31
|
+
appId: string;
|
|
32
|
+
apiToken: string;
|
|
33
|
+
}
|
|
34
|
+
declare function sfuFetch(
|
|
35
|
+
config: SFUConfig,
|
|
36
|
+
path: string,
|
|
37
|
+
body: unknown
|
|
38
|
+
): Promise<unknown>;
|
|
39
|
+
declare function createSFUSession(config: SFUConfig): Promise<{
|
|
40
|
+
sessionId: string;
|
|
41
|
+
}>;
|
|
42
|
+
declare function addSFUTracks(
|
|
43
|
+
config: SFUConfig,
|
|
44
|
+
sessionId: string,
|
|
45
|
+
body: unknown
|
|
46
|
+
): Promise<unknown>;
|
|
47
|
+
declare function renegotiateSFUSession(
|
|
48
|
+
config: SFUConfig,
|
|
49
|
+
sessionId: string,
|
|
50
|
+
sdp: string
|
|
51
|
+
): Promise<unknown>;
|
|
52
|
+
declare function createSFUWebSocketAdapter(
|
|
53
|
+
config: SFUConfig,
|
|
54
|
+
tracks: unknown[]
|
|
55
|
+
): Promise<unknown>;
|
|
56
|
+
//#endregion
|
|
57
|
+
export {
|
|
58
|
+
SFUConfig,
|
|
59
|
+
addSFUTracks,
|
|
60
|
+
createSFUSession,
|
|
61
|
+
createSFUWebSocketAdapter,
|
|
62
|
+
decodeVarint,
|
|
63
|
+
downsample48kStereoTo16kMono,
|
|
64
|
+
encodePayloadToProtobuf,
|
|
65
|
+
encodeVarint,
|
|
66
|
+
extractPayloadFromProtobuf,
|
|
67
|
+
renegotiateSFUSession,
|
|
68
|
+
sfuFetch,
|
|
69
|
+
upsample16kMonoTo48kStereo
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=sfu.d.ts.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
//#region src/voice/sfu.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pure utility functions for the Cloudflare Realtime SFU integration.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from sfu.ts for testability. These handle:
|
|
6
|
+
* - Protobuf varint encoding/decoding
|
|
7
|
+
* - SFU WebSocket adapter protobuf packet encoding/decoding
|
|
8
|
+
* - Audio format conversion (48kHz stereo ↔ 16kHz mono)
|
|
9
|
+
*/
|
|
10
|
+
function decodeVarint(buf, offset) {
|
|
11
|
+
let value = 0;
|
|
12
|
+
let shift = 0;
|
|
13
|
+
let bytesRead = 0;
|
|
14
|
+
while (offset + bytesRead < buf.length) {
|
|
15
|
+
const byte = buf[offset + bytesRead];
|
|
16
|
+
value |= (byte & 127) << shift;
|
|
17
|
+
bytesRead++;
|
|
18
|
+
if ((byte & 128) === 0) break;
|
|
19
|
+
shift += 7;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
value,
|
|
23
|
+
bytesRead
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function encodeVarint(value) {
|
|
27
|
+
const bytes = [];
|
|
28
|
+
while (value > 127) {
|
|
29
|
+
bytes.push(value & 127 | 128);
|
|
30
|
+
value >>>= 7;
|
|
31
|
+
}
|
|
32
|
+
bytes.push(value & 127);
|
|
33
|
+
return new Uint8Array(bytes);
|
|
34
|
+
}
|
|
35
|
+
/** Extract the PCM payload from a protobuf Packet message. */
|
|
36
|
+
function extractPayloadFromProtobuf(data) {
|
|
37
|
+
const buf = new Uint8Array(data);
|
|
38
|
+
let offset = 0;
|
|
39
|
+
while (offset < buf.length) {
|
|
40
|
+
const { value: tag, bytesRead: tagBytes } = decodeVarint(buf, offset);
|
|
41
|
+
offset += tagBytes;
|
|
42
|
+
const fieldNumber = tag >>> 3;
|
|
43
|
+
const wireType = tag & 7;
|
|
44
|
+
if (wireType === 0) {
|
|
45
|
+
const { bytesRead } = decodeVarint(buf, offset);
|
|
46
|
+
offset += bytesRead;
|
|
47
|
+
} else if (wireType === 2) {
|
|
48
|
+
const { value: length, bytesRead: lenBytes } = decodeVarint(buf, offset);
|
|
49
|
+
offset += lenBytes;
|
|
50
|
+
if (fieldNumber === 5) return buf.slice(offset, offset + length);
|
|
51
|
+
offset += length;
|
|
52
|
+
} else break;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** Encode PCM payload into a protobuf Packet message (for ingest/buffer mode — just payload). */
|
|
57
|
+
function encodePayloadToProtobuf(payload) {
|
|
58
|
+
const tagBytes = encodeVarint(42);
|
|
59
|
+
const lengthBytes = encodeVarint(payload.length);
|
|
60
|
+
const result = new Uint8Array(tagBytes.length + lengthBytes.length + payload.length);
|
|
61
|
+
result.set(tagBytes, 0);
|
|
62
|
+
result.set(lengthBytes, tagBytes.length);
|
|
63
|
+
result.set(payload, tagBytes.length + lengthBytes.length);
|
|
64
|
+
return result.buffer;
|
|
65
|
+
}
|
|
66
|
+
/** Downsample 48kHz stereo interleaved PCM to 16kHz mono PCM (both 16-bit LE). */
|
|
67
|
+
function downsample48kStereoTo16kMono(stereo48k) {
|
|
68
|
+
const inputView = new DataView(stereo48k.buffer, stereo48k.byteOffset, stereo48k.byteLength);
|
|
69
|
+
const inputSamples = stereo48k.byteLength / 4;
|
|
70
|
+
const outputSamples = Math.floor(inputSamples / 3);
|
|
71
|
+
const output = /* @__PURE__ */ new ArrayBuffer(outputSamples * 2);
|
|
72
|
+
const outputView = new DataView(output);
|
|
73
|
+
for (let i = 0; i < outputSamples; i++) {
|
|
74
|
+
const srcOffset = i * 3 * 4;
|
|
75
|
+
if (srcOffset + 3 >= stereo48k.byteLength) break;
|
|
76
|
+
const left = inputView.getInt16(srcOffset, true);
|
|
77
|
+
const right = inputView.getInt16(srcOffset + 2, true);
|
|
78
|
+
const mono = Math.round((left + right) / 2);
|
|
79
|
+
outputView.setInt16(i * 2, mono, true);
|
|
80
|
+
}
|
|
81
|
+
return output;
|
|
82
|
+
}
|
|
83
|
+
/** Upsample 16kHz mono PCM to 48kHz stereo interleaved PCM (both 16-bit LE). */
|
|
84
|
+
function upsample16kMonoTo48kStereo(mono16k) {
|
|
85
|
+
const inputView = new DataView(mono16k);
|
|
86
|
+
const inputSamples = mono16k.byteLength / 2;
|
|
87
|
+
const outputSamples = inputSamples * 3;
|
|
88
|
+
const output = /* @__PURE__ */ new ArrayBuffer(outputSamples * 4);
|
|
89
|
+
const outputView = new DataView(output);
|
|
90
|
+
for (let i = 0; i < inputSamples; i++) {
|
|
91
|
+
const sample = inputView.getInt16(i * 2, true);
|
|
92
|
+
for (let j = 0; j < 3; j++) {
|
|
93
|
+
const outOffset = (i * 3 + j) * 4;
|
|
94
|
+
outputView.setInt16(outOffset, sample, true);
|
|
95
|
+
outputView.setInt16(outOffset + 2, sample, true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new Uint8Array(output);
|
|
99
|
+
}
|
|
100
|
+
const SFU_API_BASE = "https://rtc.live.cloudflare.com/v1";
|
|
101
|
+
async function sfuFetch(config, path, body) {
|
|
102
|
+
const url = `${SFU_API_BASE}/apps/${config.appId}${path}`;
|
|
103
|
+
const response = await fetch(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
107
|
+
"Content-Type": "application/json"
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(body)
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const text = await response.text();
|
|
113
|
+
throw new Error(`SFU API error ${response.status}: ${text}`);
|
|
114
|
+
}
|
|
115
|
+
return response.json();
|
|
116
|
+
}
|
|
117
|
+
async function createSFUSession(config) {
|
|
118
|
+
const url = `${SFU_API_BASE}/apps/${config.appId}/sessions/new`;
|
|
119
|
+
const response = await fetch(url, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { Authorization: `Bearer ${config.apiToken}` }
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const text = await response.text();
|
|
125
|
+
throw new Error(`SFU API error ${response.status}: ${text}`);
|
|
126
|
+
}
|
|
127
|
+
return response.json();
|
|
128
|
+
}
|
|
129
|
+
async function addSFUTracks(config, sessionId, body) {
|
|
130
|
+
return sfuFetch(config, `/sessions/${sessionId}/tracks/new`, body);
|
|
131
|
+
}
|
|
132
|
+
async function renegotiateSFUSession(config, sessionId, sdp) {
|
|
133
|
+
const url = `${SFU_API_BASE}/apps/${config.appId}/sessions/${sessionId}/renegotiate`;
|
|
134
|
+
const response = await fetch(url, {
|
|
135
|
+
method: "PUT",
|
|
136
|
+
headers: {
|
|
137
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
138
|
+
"Content-Type": "application/json"
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({ sessionDescription: {
|
|
141
|
+
type: "answer",
|
|
142
|
+
sdp
|
|
143
|
+
} })
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const text = await response.text();
|
|
147
|
+
throw new Error(`SFU renegotiate error ${response.status}: ${text}`);
|
|
148
|
+
}
|
|
149
|
+
return response.json();
|
|
150
|
+
}
|
|
151
|
+
async function createSFUWebSocketAdapter(config, tracks) {
|
|
152
|
+
return sfuFetch(config, "/adapters/websocket/new", { tracks });
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { addSFUTracks, createSFUSession, createSFUWebSocketAdapter, decodeVarint, downsample48kStereoTo16kMono, encodePayloadToProtobuf, encodeVarint, extractPayloadFromProtobuf, renegotiateSFUSession, sfuFetch, upsample16kMonoTo48kStereo };
|
|
156
|
+
|
|
157
|
+
//# sourceMappingURL=sfu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sfu.js","names":[],"sources":["../../src/voice/sfu.ts"],"sourcesContent":["/**\n * Pure utility functions for the Cloudflare Realtime SFU integration.\n *\n * Extracted from sfu.ts for testability. These handle:\n * - Protobuf varint encoding/decoding\n * - SFU WebSocket adapter protobuf packet encoding/decoding\n * - Audio format conversion (48kHz stereo ↔ 16kHz mono)\n */\n\n// --- Protobuf helpers ---\n// The SFU WebSocket adapter uses a simple protobuf message:\n// message Packet {\n// uint32 sequenceNumber = 1;\n// uint32 timestamp = 2;\n// bytes payload = 5;\n// }\n\nexport function decodeVarint(\n buf: Uint8Array,\n offset: number\n): { value: number; bytesRead: number } {\n let value = 0;\n let shift = 0;\n let bytesRead = 0;\n while (offset + bytesRead < buf.length) {\n const byte = buf[offset + bytesRead];\n value |= (byte & 0x7f) << shift;\n bytesRead++;\n if ((byte & 0x80) === 0) break;\n shift += 7;\n }\n return { value, bytesRead };\n}\n\nexport function encodeVarint(value: number): Uint8Array {\n const bytes: number[] = [];\n while (value > 0x7f) {\n bytes.push((value & 0x7f) | 0x80);\n value >>>= 7;\n }\n bytes.push(value & 0x7f);\n return new Uint8Array(bytes);\n}\n\n/** Extract the PCM payload from a protobuf Packet message. */\nexport function extractPayloadFromProtobuf(\n data: ArrayBuffer\n): Uint8Array | null {\n const buf = new Uint8Array(data);\n let offset = 0;\n\n while (offset < buf.length) {\n const { value: tag, bytesRead: tagBytes } = decodeVarint(buf, offset);\n offset += tagBytes;\n\n const fieldNumber = tag >>> 3;\n const wireType = tag & 0x07;\n\n if (wireType === 0) {\n // Varint\n const { bytesRead } = decodeVarint(buf, offset);\n offset += bytesRead;\n } else if (wireType === 2) {\n // Length-delimited (bytes)\n const { value: length, bytesRead: lenBytes } = decodeVarint(buf, offset);\n offset += lenBytes;\n\n if (fieldNumber === 5) {\n // payload field\n return buf.slice(offset, offset + length);\n }\n offset += length;\n } else {\n // Unknown wire type — skip\n break;\n }\n }\n\n return null;\n}\n\n/** Encode PCM payload into a protobuf Packet message (for ingest/buffer mode — just payload). */\nexport function encodePayloadToProtobuf(payload: Uint8Array): ArrayBuffer {\n // Field 5, wire type 2 (length-delimited): tag = (5 << 3) | 2 = 42\n const tagBytes = encodeVarint(42);\n const lengthBytes = encodeVarint(payload.length);\n\n const result = new Uint8Array(\n tagBytes.length + lengthBytes.length + payload.length\n );\n result.set(tagBytes, 0);\n result.set(lengthBytes, tagBytes.length);\n result.set(payload, tagBytes.length + lengthBytes.length);\n\n return result.buffer;\n}\n\n// --- Audio conversion ---\n\n/** Downsample 48kHz stereo interleaved PCM to 16kHz mono PCM (both 16-bit LE). */\nexport function downsample48kStereoTo16kMono(\n stereo48k: Uint8Array\n): ArrayBuffer {\n // Input: 48kHz stereo 16-bit LE → 2 channels × 2 bytes = 4 bytes per sample pair\n // Output: 16kHz mono 16-bit LE → 2 bytes per sample\n // Ratio: 48000/16000 = 3, plus stereo→mono = average of L+R\n\n const inputView = new DataView(\n stereo48k.buffer,\n stereo48k.byteOffset,\n stereo48k.byteLength\n );\n const inputSamples = stereo48k.byteLength / 4; // stereo sample pairs\n const outputSamples = Math.floor(inputSamples / 3);\n const output = new ArrayBuffer(outputSamples * 2);\n const outputView = new DataView(output);\n\n for (let i = 0; i < outputSamples; i++) {\n const srcOffset = i * 3 * 4; // 3x downsample, 4 bytes per stereo pair\n if (srcOffset + 3 >= stereo48k.byteLength) break;\n const left = inputView.getInt16(srcOffset, true);\n const right = inputView.getInt16(srcOffset + 2, true);\n const mono = Math.round((left + right) / 2);\n outputView.setInt16(i * 2, mono, true);\n }\n\n return output;\n}\n\n/** Upsample 16kHz mono PCM to 48kHz stereo interleaved PCM (both 16-bit LE). */\nexport function upsample16kMonoTo48kStereo(mono16k: ArrayBuffer): Uint8Array {\n const inputView = new DataView(mono16k);\n const inputSamples = mono16k.byteLength / 2;\n const outputSamples = inputSamples * 3; // 3x upsample\n const output = new ArrayBuffer(outputSamples * 4); // stereo = 4 bytes per pair\n const outputView = new DataView(output);\n\n for (let i = 0; i < inputSamples; i++) {\n const sample = inputView.getInt16(i * 2, true);\n // Write 3 stereo samples (simple sample duplication)\n for (let j = 0; j < 3; j++) {\n const outOffset = (i * 3 + j) * 4;\n outputView.setInt16(outOffset, sample, true); // left\n outputView.setInt16(outOffset + 2, sample, true); // right\n }\n }\n\n return new Uint8Array(output);\n}\n\n// --- SFU API helpers ---\n\nexport interface SFUConfig {\n appId: string;\n apiToken: string;\n}\n\nconst SFU_API_BASE = \"https://rtc.live.cloudflare.com/v1\";\n\nexport async function sfuFetch(\n config: SFUConfig,\n path: string,\n body: unknown\n): Promise<unknown> {\n const url = `${SFU_API_BASE}/apps/${config.appId}${path}`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.apiToken}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(body)\n });\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`SFU API error ${response.status}: ${text}`);\n }\n return response.json();\n}\n\nexport async function createSFUSession(\n config: SFUConfig\n): Promise<{ sessionId: string }> {\n const url = `${SFU_API_BASE}/apps/${config.appId}/sessions/new`;\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.apiToken}`\n }\n });\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`SFU API error ${response.status}: ${text}`);\n }\n return response.json() as Promise<{ sessionId: string }>;\n}\n\nexport async function addSFUTracks(\n config: SFUConfig,\n sessionId: string,\n body: unknown\n): Promise<unknown> {\n return sfuFetch(config, `/sessions/${sessionId}/tracks/new`, body);\n}\n\nexport async function renegotiateSFUSession(\n config: SFUConfig,\n sessionId: string,\n sdp: string\n): Promise<unknown> {\n const url = `${SFU_API_BASE}/apps/${config.appId}/sessions/${sessionId}/renegotiate`;\n const response = await fetch(url, {\n method: \"PUT\",\n headers: {\n Authorization: `Bearer ${config.apiToken}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n sessionDescription: { type: \"answer\", sdp }\n })\n });\n if (!response.ok) {\n const text = await response.text();\n throw new Error(`SFU renegotiate error ${response.status}: ${text}`);\n }\n return response.json();\n}\n\nexport async function createSFUWebSocketAdapter(\n config: SFUConfig,\n tracks: unknown[]\n): Promise<unknown> {\n return sfuFetch(config, \"/adapters/websocket/new\", { tracks });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
C as VoiceTransport,
|
|
3
|
+
D as VoiceTurnSource,
|
|
4
|
+
E as VoiceTurnOutcome,
|
|
5
|
+
S as VoiceStatus,
|
|
6
|
+
T as VoiceTurnMetrics,
|
|
7
|
+
_ as VoiceErrorStage,
|
|
8
|
+
a as TranscriberSessionOptions,
|
|
9
|
+
b as VoiceRole,
|
|
10
|
+
c as VoiceAudioFormat,
|
|
11
|
+
d as VoiceCompletionOutcome,
|
|
12
|
+
f as VoiceCompletionOutcomeCode,
|
|
13
|
+
g as VoiceErrorCode,
|
|
14
|
+
h as VoiceError,
|
|
15
|
+
i as TranscriberSession,
|
|
16
|
+
l as VoiceAudioInput,
|
|
17
|
+
m as VoiceDiagnosticsOptions,
|
|
18
|
+
n as TTSProvider,
|
|
19
|
+
o as TranscriptMessage,
|
|
20
|
+
p as VoiceDiagnosticEvent,
|
|
21
|
+
r as Transcriber,
|
|
22
|
+
s as VOICE_PROTOCOL_VERSION,
|
|
23
|
+
t as StreamingTTSProvider,
|
|
24
|
+
u as VoiceClientMessage,
|
|
25
|
+
v as VoiceModelFinishReason,
|
|
26
|
+
w as VoiceTransportCloseInfo,
|
|
27
|
+
x as VoiceServerMessage,
|
|
28
|
+
y as VoicePipelineMetrics
|
|
29
|
+
} from "../types-_Faxb570.js";
|
|
30
|
+
export {
|
|
31
|
+
StreamingTTSProvider,
|
|
32
|
+
TTSProvider,
|
|
33
|
+
Transcriber,
|
|
34
|
+
TranscriberSession,
|
|
35
|
+
TranscriberSessionOptions,
|
|
36
|
+
TranscriptMessage,
|
|
37
|
+
VOICE_PROTOCOL_VERSION,
|
|
38
|
+
VoiceAudioFormat,
|
|
39
|
+
VoiceAudioInput,
|
|
40
|
+
VoiceClientMessage,
|
|
41
|
+
VoiceCompletionOutcome,
|
|
42
|
+
VoiceCompletionOutcomeCode,
|
|
43
|
+
VoiceDiagnosticEvent,
|
|
44
|
+
VoiceDiagnosticsOptions,
|
|
45
|
+
VoiceError,
|
|
46
|
+
VoiceErrorCode,
|
|
47
|
+
VoiceErrorStage,
|
|
48
|
+
VoiceModelFinishReason,
|
|
49
|
+
VoicePipelineMetrics,
|
|
50
|
+
VoiceRole,
|
|
51
|
+
VoiceServerMessage,
|
|
52
|
+
VoiceStatus,
|
|
53
|
+
VoiceTransport,
|
|
54
|
+
VoiceTransportCloseInfo,
|
|
55
|
+
VoiceTurnMetrics,
|
|
56
|
+
VoiceTurnOutcome,
|
|
57
|
+
VoiceTurnSource
|
|
58
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/voice/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared types for the voice pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Used by both the server (index.ts) and client (client.ts)
|
|
6
|
+
* to ensure protocol consistency.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Current voice protocol version.
|
|
10
|
+
* Bump this when making backwards-incompatible wire protocol changes.
|
|
11
|
+
* The server sends this in the initial `welcome` message so clients
|
|
12
|
+
* can detect version mismatches.
|
|
13
|
+
*/
|
|
14
|
+
const VOICE_PROTOCOL_VERSION = 1;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { VOICE_PROTOCOL_VERSION };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../src/voice/types.ts"],"sourcesContent":["/**\n * Shared types for the voice pipeline.\n *\n * Used by both the server (index.ts) and client (client.ts)\n * to ensure protocol consistency.\n */\n\n// --- Protocol version ---\n\n/**\n * Current voice protocol version.\n * Bump this when making backwards-incompatible wire protocol changes.\n * The server sends this in the initial `welcome` message so clients\n * can detect version mismatches.\n */\nexport const VOICE_PROTOCOL_VERSION = 1;\n\n// --- Voice status ---\n\nexport type VoiceStatus = \"idle\" | \"listening\" | \"thinking\" | \"speaking\";\n\n// --- Diagnostics ---\n\n/** Server-side diagnostic configuration shared by both voice mixins. */\nexport interface VoiceDiagnosticsOptions {\n /** Forward safe server diagnostics and enable browser console logging. */\n browserConsole?: boolean;\n}\n\n/**\n * A bounded diagnostic record forwarded by the voice protocol.\n * Event names and metadata are intentionally open and are not stable API.\n */\nexport interface VoiceDiagnosticEvent {\n event: string;\n timestamp: number;\n data?: Record<string, unknown>;\n}\n\n// --- Stable per-turn metrics ---\n\nexport type VoiceTurnSource = \"speech\" | \"text\";\n\nexport type VoiceTurnOutcome =\n | \"completed\"\n | \"no_output\"\n | \"output_limit\"\n | \"content_filtered\"\n | \"model_error\"\n | \"tts_error\"\n | \"aborted\"\n | \"skipped\"\n | \"error\";\n\n/**\n * Stable, content-free summary of one allocated voice turn.\n *\n * `turnId`, `source`, and `outcome` are dimensions used to correlate and\n * interpret the measurements; every other field is a duration. Optional\n * timings are omitted when their lifecycle landmark was not reached. Timings\n * use one server clock, overlap, and are not additive. Browser playback timing\n * is excluded because browser and Worker clocks are independent.\n */\nexport interface VoiceTurnMetrics {\n /** SDK-assigned correlation ID for this turn. */\n turnId: string;\n /** Whether the turn originated from finalized speech or a text message. */\n source: VoiceTurnSource;\n /** Terminal result, emitted exactly once for the allocated turn. */\n outcome: VoiceTurnOutcome;\n /** Turn allocation to terminal summary, in milliseconds. */\n turnTotalMs: number;\n /** Provider speech start to the first interim transcript. */\n speechStartToFirstInterimMs?: number;\n /** Provider speech start to the finalized transcript. */\n speechStartToFinalMs?: number;\n /** Time spent in the server's `afterTranscribe` hook. */\n afterTranscribeMs?: number;\n /** Model invocation to the first non-whitespace text delta. */\n modelToFirstTextMs?: number;\n /** Cumulative duration of reasoning blocks exposed by the model stream. */\n exposedReasoningMs?: number;\n /** Model invocation through normalized stream consumption. */\n modelStreamConsumptionMs?: number;\n /** Finalized input through the first server audio send. */\n finalInputToFirstAudioMs?: number;\n\n /** First TTS provider invocation through the first server audio send. */\n ttsToFirstAudioMs?: number;\n /** First TTS provider invocation through completion of all sentence work. */\n ttsWallMs?: number;\n /** Cumulative overlapping TTS sentence hook and provider work. */\n ttsWorkMs?: number;\n}\n\n// --- Audio format ---\n\n/** Audio format the server uses for binary audio payloads. */\nexport type VoiceAudioFormat = \"mp3\" | \"pcm16\" | \"wav\" | \"opus\";\n\n// --- Conversation message role ---\n\nexport type VoiceRole = \"user\" | \"assistant\";\n\n// --- Structured voice errors ---\n\n/** Stable machine-readable error codes emitted by the voice protocol. */\nexport type VoiceErrorCode = \"stt_startup_failed\" | \"stt_connection_lost\";\n\n/** Stable pipeline stage associated with a structured voice error. */\nexport type VoiceErrorStage = \"stt\";\n\n/** Client-safe error detail. `message` remains for string-event compatibility. */\nexport interface VoiceError {\n message: string;\n code?: VoiceErrorCode;\n stage?: VoiceErrorStage;\n retryable?: boolean;\n}\n\n// --- LLM completion outcomes ---\n\n/** Stable machine-readable outcomes for non-ordinary LLM completions. */\nexport type VoiceCompletionOutcomeCode =\n | \"no_output\"\n | \"output_limit\"\n | \"content_filtered\"\n | \"model_error\";\n\n/** Normalized finish reasons emitted by the supported AI SDK stream shape. */\nexport type VoiceModelFinishReason =\n | \"stop\"\n | \"length\"\n | \"content-filter\"\n | \"tool-calls\"\n | \"error\"\n | \"other\";\n\n/** Bounded completion metadata exposed by the voice protocol. */\nexport interface VoiceCompletionOutcome {\n code: VoiceCompletionOutcomeCode;\n stage: \"llm\";\n finishReason?: VoiceModelFinishReason;\n partialOutput: boolean;\n}\n\n// --- Wire protocol: Client → Server ---\n\nexport type VoiceClientMessage =\n | { type: \"hello\"; protocol_version?: number }\n | { type: \"start_call\"; preferred_format?: VoiceAudioFormat }\n | { type: \"end_call\" }\n | { type: \"start_of_speech\" }\n | { type: \"end_of_speech\" }\n | { type: \"interrupt\" }\n | { type: \"text_message\"; text: string };\n\n// --- Wire protocol: Server → Client ---\n\nexport type VoiceServerMessage =\n | {\n type: \"welcome\";\n protocol_version: number;\n diagnostics?: { browser_console: true };\n }\n | ({ type: \"diagnostic\" } & VoiceDiagnosticEvent)\n | { type: \"status\"; status: VoiceStatus }\n | { type: \"audio_config\"; format: VoiceAudioFormat; sampleRate?: number }\n | { type: \"transcript\"; role: VoiceRole; text: string }\n | { type: \"transcript_start\"; role: VoiceRole }\n | { type: \"transcript_delta\"; text: string }\n | { type: \"transcript_end\"; text: string }\n | { type: \"transcript_interim\"; text: string }\n | { type: \"playback_interrupt\" }\n | {\n type: \"metrics\";\n llm_ms: number;\n tts_ms: number;\n first_audio_ms: number;\n total_ms: number;\n }\n | ({ type: \"turn_metrics\" } & VoiceTurnMetrics)\n | ({ type: \"completion_outcome\" } & VoiceCompletionOutcome)\n | ({ type: \"error\" } & VoiceError);\n\n// --- Pipeline metrics (structured form for consumers) ---\n\n/**\n * Compact compatibility summary for successful, non-empty speech turns.\n * These overlapping latency landmarks and work totals are not additive.\n * Use `VoiceTurnMetrics` for stable detailed timing and terminal summaries of\n * unsuccessful, aborted, skipped, or text turns.\n */\nexport interface VoicePipelineMetrics {\n /**\n * Time from immediately before `onTurn()` until normalized model-stream\n * consumption completes. This can include tool work and consumer waits while\n * consuming the stream. It is not time to first text.\n */\n llm_ms: number;\n /**\n * Cumulative per-sentence work from immediately before `beforeSynthesize`\n * until that sentence's synthesis and hook work settles. Sentence work can\n * overlap both other sentences and model consumption, so this value can\n * exceed wall time and overlaps the other metrics.\n */\n tts_ms: number;\n /**\n * Time from turn-pipeline start, before `afterTranscribe`, to the first server\n * audio send. Includes post-STT hooks, model work, and TTS, but excludes STT\n * and browser playback. `0` means the server sent no audio.\n */\n first_audio_ms: number;\n /**\n * Time from the same turn-pipeline start until model consumption and TTS\n * draining complete. Measured before final context, persistence, and status\n * work. Excludes STT and browser playback.\n */\n total_ms: number;\n}\n\n// --- Transcript message (client-side enriched form) ---\n\nexport interface TranscriptMessage {\n role: VoiceRole;\n text: string;\n timestamp: number;\n}\n\n// --- Provider interfaces ---\n\nexport interface TTSProvider {\n synthesize(text: string, signal?: AbortSignal): Promise<ArrayBuffer | null>;\n}\n\nexport interface StreamingTTSProvider {\n synthesizeStream(\n text: string,\n signal?: AbortSignal\n ): AsyncGenerator<ArrayBuffer>;\n}\n\n// --- Transcriber (continuous per-call STT) ---\n\n/**\n * Continuous speech-to-text provider.\n *\n * Creates a per-call session that receives audio continuously from\n * `start_call` to `end_call`. The model handles turn detection\n * internally — there is no client-side speech boundary signaling\n * required for STT.\n *\n * The session fires `onUtterance` when the model detects a complete\n * utterance (e.g. Flux `EndOfTurn`, Nova 3 `speech_final` +\n * endpointing). The voice pipeline maps this to `onTurn` (withVoice)\n * or `onTranscript` (withVoiceInput).\n */\nexport interface Transcriber {\n /** Create a new transcription session for one call. */\n createSession(options?: TranscriberSessionOptions): TranscriberSession;\n}\n\nexport interface TranscriberSessionOptions {\n /** Language code (e.g. \"en\"). */\n language?: string;\n /**\n * Called when the provider produces an interim (unstable) transcript.\n * This text may change as more audio arrives.\n */\n onInterim?: (text: string) => void;\n /**\n * Called when the model detects the start of user speech.\n *\n * Providers can use this for low-latency barge-in before a final\n * utterance is available. The transcript may be omitted or unstable.\n */\n onSpeechStart?: (text?: string) => void;\n /**\n * Called when the model detects a complete utterance.\n * The transcript is the stable text for this turn.\n *\n * For Flux: fires on `EndOfTurn`.\n * For Nova 3: fires on `Results` with `speech_final: true`.\n */\n onUtterance?: (transcript: string) => void;\n /**\n * Called when the session can no longer transcribe because its provider\n * connection failed or closed unexpectedly. Providers must not call this\n * for teardown initiated by {@link TranscriberSession.close}.\n */\n onFatalError?: (error: Error) => void;\n}\n\n/**\n * A per-call transcription session. Lives for the entire call duration.\n *\n * Unlike per-utterance sessions, this session is never finished or\n * aborted mid-call. It receives all audio continuously and the model\n * handles speech boundary detection. On interrupt, the LLM+TTS\n * pipeline is aborted but the transcriber session stays alive.\n */\nexport interface TranscriberSession {\n /**\n * Feed raw PCM audio (16kHz mono 16-bit LE).\n * Fire-and-forget — the session buffers internally as needed.\n */\n feed(chunk: ArrayBuffer): void;\n\n /**\n * Resolves when the session is ready to accept audio and emit transcripts.\n * Optional so existing custom transcribers can start synchronously.\n */\n waitUntilReady?(): Promise<void>;\n\n /**\n * Optional. Provide the agent's most recent spoken reply (the text sent to\n * TTS) as conversational context for the next user turn.\n *\n * The pipeline calls this after the agent finishes speaking each reply and\n * greeting. Providers that support context carryover (e.g. AssemblyAI's\n * `agent_context`) use it to better recognize short or contextual answers\n * (\"yes\", \"7pm\", an email spelled aloud). Providers that don't support it\n * simply omit this method — it is a no-op for them.\n */\n updateAgentContext?(text: string): void;\n\n /**\n * Close the session and release resources.\n * Called at end_call or disconnect — not on interrupt.\n */\n close(): void;\n}\n\n// --- Audio input ---\n\n/**\n * Pluggable audio input source for VoiceClient.\n *\n * When provided via `VoiceClientOptions.audioInput`, VoiceClient delegates\n * mic capture to this object instead of using its built-in AudioWorklet.\n * The audio input is responsible for capturing audio and routing it to the\n * server (however it chooses — WebRTC, SFU, direct binary, etc.).\n *\n * It must call `onAudioLevel` with RMS values so VoiceClient can run\n * silence detection, interrupt detection, and update the audio level UI.\n *\n * @example\n * ```typescript\n * class SFUAudioInput implements VoiceAudioInput {\n * onAudioLevel: ((rms: number) => void) | null = null;\n * async start() {\n * // Set up WebRTC peer connection, SFU session, etc.\n * // In a monitoring loop, call this.onAudioLevel?.(rms)\n * }\n * stop() {\n * // Tear down WebRTC\n * }\n * }\n * ```\n */\nexport interface VoiceAudioInput {\n /** Start capturing audio. Called by VoiceClient on startCall(). */\n start(): Promise<void>;\n /** Stop capturing audio. Called by VoiceClient on endCall() or disconnect(). */\n stop(): void;\n /**\n * Set by VoiceClient before start(). The audio input must call this\n * with RMS audio level values on each frame so VoiceClient can run\n * silence detection, interrupt detection, and update the UI.\n */\n onAudioLevel: ((rms: number) => void) | null;\n\n /**\n * Set by VoiceClient before start(). If the audio input provides\n * raw PCM audio (16kHz mono 16-bit LE), call this callback and\n * VoiceClient will forward the data to the server via its transport.\n *\n * This is needed when audio reaches the server through the same\n * WebSocket as protocol messages (e.g. SFU in local dev where the\n * SFU adapter can't connect back to localhost).\n *\n * If the audio input routes audio to the server through an external\n * path (e.g. SFU WebSocket adapter in production), this can be left\n * unused — the audio will arrive on a separate connection.\n */\n onAudioData?: ((pcm: ArrayBuffer) => void) | null;\n}\n\n// --- Voice transport ---\n\n/** Details a transport can provide when its connection closes. */\nexport interface VoiceTransportCloseInfo {\n code?: number;\n reason?: string;\n wasClean?: boolean;\n}\n\n/**\n * Abstraction over the data channel between client and server.\n * The default implementation wraps PartySocket (WebSocket).\n * Implement this interface to use WebRTC, SFU, or other transports.\n */\nexport interface VoiceTransport {\n /** Send a JSON-serializable message to the server. */\n sendJSON(data: Record<string, unknown>): void;\n /** Send raw binary audio to the server. */\n sendBinary(data: ArrayBuffer): void;\n\n /** Open the connection. */\n connect(): void;\n /** Close the connection and release resources. */\n disconnect(): void;\n\n /** Whether the transport is currently connected and ready to send. */\n readonly connected: boolean;\n\n // --- Event callbacks (set by VoiceClient) ---\n onopen: (() => void) | null;\n onclose: ((info?: VoiceTransportCloseInfo) => void) | null;\n onerror: ((error?: unknown) => void) | null;\n /** Called when a JSON string message arrives from the server. */\n onmessage: ((data: string | ArrayBuffer | Blob) => void) | null;\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAa,yBAAyB"}
|