osborn 0.9.40 → 0.9.41
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/dist/index.js +240 -6
- package/dist/recall-client.js +38 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Load environment variables FIRST before any other imports
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { voice, initializeLogger } from '@livekit/agents';
|
|
4
|
-
import { Room, RoomEvent } from '@livekit/rtc-node';
|
|
4
|
+
import { Room, RoomEvent, AudioSource, AudioFrame, LocalAudioTrack, TrackPublishOptions, TrackSource, } from '@livekit/rtc-node';
|
|
5
5
|
import { AccessToken } from 'livekit-server-sdk';
|
|
6
6
|
// Initialize logger before anything else
|
|
7
7
|
initializeLogger({ pretty: true, level: 'info' });
|
|
@@ -149,6 +149,12 @@ process.on('uncaughtException', (error) => {
|
|
|
149
149
|
let currentRoomCode = null;
|
|
150
150
|
// Meeting output WebSocket — module-level so both startApiServer and main() can access it
|
|
151
151
|
let meetingOutputWs = null;
|
|
152
|
+
// Module-level AgentSession reference so /meeting-audio-in WS handler can switch
|
|
153
|
+
// the RoomIO-linked participant when meeting audio starts/stops (B2 design).
|
|
154
|
+
let activeAgentSession = null;
|
|
155
|
+
// Identity of the local user participant the session was originally listening to
|
|
156
|
+
// — captured at the moment we switch to the meeting publisher, restored on cleanup.
|
|
157
|
+
let preMeetingUserIdentity = null;
|
|
152
158
|
function sendToMeetingOutput(msg) {
|
|
153
159
|
if (meetingOutputWs && meetingOutputWs.readyState === WebSocket.OPEN) {
|
|
154
160
|
try {
|
|
@@ -960,20 +966,235 @@ function startApiServer(workingDir, port) {
|
|
|
960
966
|
cleanStaleUploadDirs();
|
|
961
967
|
setInterval(cleanStaleUploadDirs, 10 * 60 * 1000);
|
|
962
968
|
// ============================================================
|
|
963
|
-
// Meeting Output WebSocket — /meeting-audio
|
|
969
|
+
// Meeting Output WebSocket — /meeting-audio (LEGACY)
|
|
964
970
|
// ============================================================
|
|
965
|
-
// Recall's headless browser
|
|
966
|
-
//
|
|
971
|
+
// Recall's headless browser used to open meeting-output.html which connects
|
|
972
|
+
// here. With the new /meeting-bot Next.js page (Phase 2 + LiveKit), Recall
|
|
973
|
+
// points at frontend/meeting-bot instead — this handler exists only for
|
|
974
|
+
// backwards-compat with old machine images still serving the legacy path.
|
|
967
975
|
const meetingOutputWss = new WebSocketServer({ noServer: true });
|
|
968
976
|
meetingOutputWss.on('connection', (ws) => {
|
|
969
|
-
console.log('📺 Meeting output browser connected');
|
|
977
|
+
console.log('📺 Meeting output browser connected (legacy /meeting-audio)');
|
|
970
978
|
meetingOutputWs = ws;
|
|
971
979
|
ws.on('close', () => {
|
|
972
|
-
console.log('📺 Meeting output browser disconnected');
|
|
980
|
+
console.log('📺 Meeting output browser disconnected (legacy)');
|
|
973
981
|
if (meetingOutputWs === ws)
|
|
974
982
|
meetingOutputWs = null;
|
|
975
983
|
});
|
|
976
984
|
});
|
|
985
|
+
// ============================================================
|
|
986
|
+
// Recall.ai meeting-audio-in WebSocket — /meeting-audio-in
|
|
987
|
+
// ============================================================
|
|
988
|
+
// Recall.ai's per-participant real-time audio protocol. Bot is configured
|
|
989
|
+
// (in recall-client.ts joinMeeting) with audio_separate_raw + a realtime
|
|
990
|
+
// endpoint pointing at this URL. Recall sends JSON events containing
|
|
991
|
+
// base64-encoded PCM (S16LE, 16kHz, mono) for every meeting participant
|
|
992
|
+
// (bot's own audio NOT included by default — no feedback loop possible).
|
|
993
|
+
//
|
|
994
|
+
// Flow: Recall → /meeting-audio-in → open a SECOND LiveKit connection from
|
|
995
|
+
// this agent process as a publisher participant → publish PCM as an
|
|
996
|
+
// audio track in the same LiveKit room → the existing AgentSession's
|
|
997
|
+
// STT subscribes to it as a remote track → routes to currentLLM.chat()
|
|
998
|
+
// via the same pipeline as voice-native user mic.
|
|
999
|
+
//
|
|
1000
|
+
// The advantage of this design vs a parallel STT pipeline: meeting audio
|
|
1001
|
+
// becomes "just another participant" in the LiveKit room — same end-of-turn
|
|
1002
|
+
// detection, same interrupt handling, same conversation context, no parallel
|
|
1003
|
+
// chat() paths to maintain.
|
|
1004
|
+
//
|
|
1005
|
+
// Wait until activeAgentSession._roomIO exists AND the publisher participant
|
|
1006
|
+
// is visible to the agent's room. Both can race against join_meeting:
|
|
1007
|
+
// - Agent session may still be starting up when Recall connects.
|
|
1008
|
+
// - LiveKit takes a moment to propagate the publisher's join to the agent
|
|
1009
|
+
// side after publishTrack() returns on our side.
|
|
1010
|
+
// Bounded poll (200ms cadence) avoids both timing gaps.
|
|
1011
|
+
async function waitForRoomIOAndParticipant(publisherIdentity, timeoutMs) {
|
|
1012
|
+
const deadline = Date.now() + timeoutMs;
|
|
1013
|
+
let roomIO = null;
|
|
1014
|
+
let participantVisible = false;
|
|
1015
|
+
while (Date.now() < deadline) {
|
|
1016
|
+
roomIO = activeAgentSession?._roomIO;
|
|
1017
|
+
if (roomIO && typeof roomIO.setParticipant === 'function') {
|
|
1018
|
+
const agentRoom = roomIO.rtcRoom;
|
|
1019
|
+
const remotes = agentRoom?.remoteParticipants;
|
|
1020
|
+
if (remotes && typeof remotes.values === 'function') {
|
|
1021
|
+
for (const p of remotes.values()) {
|
|
1022
|
+
if (p?.identity === publisherIdentity) {
|
|
1023
|
+
participantVisible = true;
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (participantVisible)
|
|
1029
|
+
return { roomIO, participantVisible };
|
|
1030
|
+
}
|
|
1031
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1032
|
+
}
|
|
1033
|
+
// Timed out — return whatever we have. Caller decides whether to proceed.
|
|
1034
|
+
return { roomIO, participantVisible };
|
|
1035
|
+
}
|
|
1036
|
+
const meetingAudioInWss = new WebSocketServer({ noServer: true });
|
|
1037
|
+
meetingAudioInWss.on('connection', async (recallWs) => {
|
|
1038
|
+
console.log('🎙️ Recall audio-in WebSocket connected — setting up LiveKit publisher');
|
|
1039
|
+
const livekitUrl = process.env.LIVEKIT_URL;
|
|
1040
|
+
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
1041
|
+
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
1042
|
+
if (!livekitUrl || !apiKey || !apiSecret) {
|
|
1043
|
+
console.warn('⚠️ LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET not set — meeting audio publisher disabled');
|
|
1044
|
+
recallWs.close();
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
if (!currentRoomCode) {
|
|
1048
|
+
console.warn('⚠️ No active LiveKit room (currentRoomCode null) — meeting audio publisher cannot attach');
|
|
1049
|
+
recallWs.close();
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const roomName = `osborn-${currentRoomCode}`;
|
|
1053
|
+
// Mint a publisher token via livekit-server-sdk (already imported for
|
|
1054
|
+
// /api/token style flows). Long TTL — meetings can run for hours.
|
|
1055
|
+
const identity = `meeting-audio-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1056
|
+
const at = new AccessToken(apiKey, apiSecret, {
|
|
1057
|
+
identity,
|
|
1058
|
+
ttl: 14400, // 4 hours
|
|
1059
|
+
metadata: JSON.stringify({ role: 'meeting-audio-publisher' }),
|
|
1060
|
+
});
|
|
1061
|
+
at.addGrant({ roomJoin: true, room: roomName, canPublish: true, canSubscribe: false });
|
|
1062
|
+
const token = await at.toJwt();
|
|
1063
|
+
let room = null;
|
|
1064
|
+
let source = null;
|
|
1065
|
+
let track = null;
|
|
1066
|
+
const cleanup = async () => {
|
|
1067
|
+
// Restore AgentSession STT input to the original user participant before
|
|
1068
|
+
// tearing down the publisher track. If we don't switch back, the session
|
|
1069
|
+
// will be stuck waiting on a participant that's about to disappear.
|
|
1070
|
+
try {
|
|
1071
|
+
const roomIO = activeAgentSession?._roomIO;
|
|
1072
|
+
if (roomIO && typeof roomIO.setParticipant === 'function') {
|
|
1073
|
+
if (preMeetingUserIdentity) {
|
|
1074
|
+
roomIO.setParticipant(preMeetingUserIdentity);
|
|
1075
|
+
console.log(`🔁 Restored AgentSession STT input to user: ${preMeetingUserIdentity}`);
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
roomIO.unsetParticipant();
|
|
1079
|
+
console.log('🔁 Cleared AgentSession STT input (no original user to restore)');
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch (err) {
|
|
1084
|
+
console.warn('⚠️ Failed to restore RoomIO participant on cleanup:', err.message);
|
|
1085
|
+
}
|
|
1086
|
+
preMeetingUserIdentity = null;
|
|
1087
|
+
try {
|
|
1088
|
+
if (track)
|
|
1089
|
+
await track.close(true);
|
|
1090
|
+
}
|
|
1091
|
+
catch { }
|
|
1092
|
+
try {
|
|
1093
|
+
if (source)
|
|
1094
|
+
await source.close();
|
|
1095
|
+
}
|
|
1096
|
+
catch { }
|
|
1097
|
+
try {
|
|
1098
|
+
if (room)
|
|
1099
|
+
await room.disconnect();
|
|
1100
|
+
}
|
|
1101
|
+
catch { }
|
|
1102
|
+
room = null;
|
|
1103
|
+
source = null;
|
|
1104
|
+
track = null;
|
|
1105
|
+
};
|
|
1106
|
+
try {
|
|
1107
|
+
room = new Room();
|
|
1108
|
+
await room.connect(livekitUrl, token);
|
|
1109
|
+
if (!room.localParticipant)
|
|
1110
|
+
throw new Error('LiveKit connected but localParticipant missing');
|
|
1111
|
+
// Recall sends S16LE PCM at 16kHz mono. AudioSource matches the format.
|
|
1112
|
+
source = new AudioSource(16000, 1);
|
|
1113
|
+
track = LocalAudioTrack.createAudioTrack('meeting-audio', source);
|
|
1114
|
+
await room.localParticipant.publishTrack(track, new TrackPublishOptions({ source: TrackSource.SOURCE_MICROPHONE }));
|
|
1115
|
+
console.log(`🎙️ Meeting audio publisher connected to ${roomName} as ${identity}`);
|
|
1116
|
+
// B2 — switch the existing AgentSession's RoomIO input from the local user
|
|
1117
|
+
// to this meeting-audio publisher. While the meeting is active, the user
|
|
1118
|
+
// talks via the meeting (Recall captures it and sends PCM here), and the
|
|
1119
|
+
// agent treats this publisher as the "speaking" participant for STT/EOT.
|
|
1120
|
+
// Original user identity is stashed so cleanup() can restore it.
|
|
1121
|
+
//
|
|
1122
|
+
// 15s timeout accommodates: session-start race (agent still booting when
|
|
1123
|
+
// user clicks "join meeting"), LiveKit participant-join propagation
|
|
1124
|
+
// (~hundreds of ms), and Fly cold-path latency on first request.
|
|
1125
|
+
try {
|
|
1126
|
+
const { roomIO, participantVisible } = await waitForRoomIOAndParticipant(identity, 15000);
|
|
1127
|
+
if (!roomIO) {
|
|
1128
|
+
console.warn('⚠️ Timed out waiting for AgentSession._roomIO (15s) — meeting audio published but STT not switched. Meeting audio will be ignored until a session starts.');
|
|
1129
|
+
}
|
|
1130
|
+
else if (!participantVisible) {
|
|
1131
|
+
// RoomIO exists but our publisher hasn't propagated to the agent's
|
|
1132
|
+
// room view yet. setParticipant stores the identity and links on
|
|
1133
|
+
// participant-connected event, so this is still safe to call —
|
|
1134
|
+
// RoomIO will pick up the link when the event arrives.
|
|
1135
|
+
preMeetingUserIdentity = roomIO.linkedParticipant?.identity ?? null;
|
|
1136
|
+
roomIO.setParticipant(identity);
|
|
1137
|
+
console.log(`🔁 Switched AgentSession STT input (publisher not yet visible — will link on connect): ${preMeetingUserIdentity ?? '(none)'} → ${identity}`);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
preMeetingUserIdentity = roomIO.linkedParticipant?.identity ?? null;
|
|
1141
|
+
roomIO.setParticipant(identity);
|
|
1142
|
+
console.log(`🔁 Switched AgentSession STT input: ${preMeetingUserIdentity ?? '(none)'} → ${identity}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
console.warn('⚠️ Failed to switch RoomIO participant:', err.message);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
catch (err) {
|
|
1150
|
+
console.error('❌ Failed to set up LiveKit publisher for meeting audio:', err instanceof Error ? err.message : err);
|
|
1151
|
+
try {
|
|
1152
|
+
recallWs.close();
|
|
1153
|
+
}
|
|
1154
|
+
catch { }
|
|
1155
|
+
await cleanup();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
// Recall → us: JSON events with base64-encoded PCM. Decode, wrap as
|
|
1159
|
+
// AudioFrame, and capture into the source. AgentSession in the main room
|
|
1160
|
+
// will subscribe to this published track and STT it via the normal pipeline.
|
|
1161
|
+
// Payload shape from
|
|
1162
|
+
// docs.recall.ai/docs/how-to-get-separate-audio-per-participant-realtime:
|
|
1163
|
+
// { event: 'audio_separate_raw.data', data: { data: { buffer: '<base64>', ... }, participant: {...} } }
|
|
1164
|
+
recallWs.on('message', async (raw) => {
|
|
1165
|
+
if (!source)
|
|
1166
|
+
return;
|
|
1167
|
+
try {
|
|
1168
|
+
const msg = JSON.parse(raw.toString());
|
|
1169
|
+
if (msg.event !== 'audio_separate_raw.data')
|
|
1170
|
+
return;
|
|
1171
|
+
const b64 = msg.data?.data?.buffer;
|
|
1172
|
+
if (!b64)
|
|
1173
|
+
return;
|
|
1174
|
+
const pcmBuf = Buffer.from(b64, 'base64');
|
|
1175
|
+
// AudioFrame expects Int16Array. The PCM buffer is S16LE — view it
|
|
1176
|
+
// directly without copy. Length / 2 = samples (each sample 2 bytes).
|
|
1177
|
+
const samplesPerChannel = pcmBuf.byteLength / 2;
|
|
1178
|
+
const int16 = new Int16Array(pcmBuf.buffer, pcmBuf.byteOffset, samplesPerChannel);
|
|
1179
|
+
const frame = new AudioFrame(int16, 16000, 1, samplesPerChannel);
|
|
1180
|
+
await source.captureFrame(frame);
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
// Don't log every frame parse failure — could be noisy if Recall sends
|
|
1184
|
+
// non-audio_separate_raw events on the same channel.
|
|
1185
|
+
if (err.message?.includes('JSON'))
|
|
1186
|
+
return;
|
|
1187
|
+
console.warn('⚠️ meeting audio capture error:', err instanceof Error ? err.message : err);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
recallWs.on('close', async () => {
|
|
1191
|
+
console.log('🎙️ Recall audio-in WebSocket closed — tearing down LiveKit publisher');
|
|
1192
|
+
await cleanup();
|
|
1193
|
+
});
|
|
1194
|
+
recallWs.on('error', (err) => {
|
|
1195
|
+
console.warn('⚠️ Recall WS error:', err instanceof Error ? err.message : err);
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
977
1198
|
server.on('upgrade', (req, socket, head) => {
|
|
978
1199
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
979
1200
|
if (url.pathname === '/meeting-audio') {
|
|
@@ -981,6 +1202,11 @@ function startApiServer(workingDir, port) {
|
|
|
981
1202
|
meetingOutputWss.emit('connection', ws, req);
|
|
982
1203
|
});
|
|
983
1204
|
}
|
|
1205
|
+
else if (url.pathname === '/meeting-audio-in') {
|
|
1206
|
+
meetingAudioInWss.handleUpgrade(req, socket, head, (ws) => {
|
|
1207
|
+
meetingAudioInWss.emit('connection', ws, req);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
984
1210
|
else {
|
|
985
1211
|
socket.destroy();
|
|
986
1212
|
}
|
|
@@ -2630,6 +2856,7 @@ async function main() {
|
|
|
2630
2856
|
}
|
|
2631
2857
|
lastCompletedResearch = null;
|
|
2632
2858
|
currentSession = null;
|
|
2859
|
+
activeAgentSession = null;
|
|
2633
2860
|
currentAgent = null;
|
|
2634
2861
|
// Same disconnect-leak fix as the other two cleanup sites — kill the Claude SDK
|
|
2635
2862
|
// subprocess BEFORE dropping the reference. See killCurrentLLM() for full context.
|
|
@@ -2675,6 +2902,7 @@ async function main() {
|
|
|
2675
2902
|
}
|
|
2676
2903
|
catch { }
|
|
2677
2904
|
currentSession = null;
|
|
2905
|
+
activeAgentSession = null;
|
|
2678
2906
|
currentAgent = null;
|
|
2679
2907
|
// Same disconnect-leak fix — kill the previous user's Claude subprocess
|
|
2680
2908
|
// before binding currentLLM to the new user's session below.
|
|
@@ -2829,6 +3057,7 @@ async function main() {
|
|
|
2829
3057
|
agent = result.agent;
|
|
2830
3058
|
}
|
|
2831
3059
|
currentSession = session;
|
|
3060
|
+
activeAgentSession = session;
|
|
2832
3061
|
currentAgent = agent; // Store for updateChatCtx() context injection
|
|
2833
3062
|
// ============================================================
|
|
2834
3063
|
// Session event wiring — extracted into function for auto-recovery
|
|
@@ -2988,6 +3217,7 @@ async function main() {
|
|
|
2988
3217
|
}
|
|
2989
3218
|
catch { }
|
|
2990
3219
|
currentSession = null;
|
|
3220
|
+
activeAgentSession = null;
|
|
2991
3221
|
currentAgent = null;
|
|
2992
3222
|
// Clear stale state from crashed session
|
|
2993
3223
|
voiceQueue.length = 0;
|
|
@@ -3049,6 +3279,7 @@ async function main() {
|
|
|
3049
3279
|
const newSession = result.session;
|
|
3050
3280
|
const newAgent = result.agent;
|
|
3051
3281
|
currentSession = newSession;
|
|
3282
|
+
activeAgentSession = newSession;
|
|
3052
3283
|
currentAgent = newAgent;
|
|
3053
3284
|
// Re-wire event listeners on the new session
|
|
3054
3285
|
wireSessionEvents(newSession, newAgent);
|
|
@@ -3105,6 +3336,7 @@ async function main() {
|
|
|
3105
3336
|
}
|
|
3106
3337
|
catch { }
|
|
3107
3338
|
currentSession = null;
|
|
3339
|
+
activeAgentSession = null;
|
|
3108
3340
|
currentAgent = null;
|
|
3109
3341
|
// Clear voice queue — stale injections from the crashed session
|
|
3110
3342
|
voiceQueue.length = 0;
|
|
@@ -3128,6 +3360,7 @@ async function main() {
|
|
|
3128
3360
|
const newSession = result.session;
|
|
3129
3361
|
const newAgent = result.agent;
|
|
3130
3362
|
currentSession = newSession;
|
|
3363
|
+
activeAgentSession = newSession;
|
|
3131
3364
|
currentAgent = newAgent;
|
|
3132
3365
|
// Re-wire event listeners on the new session
|
|
3133
3366
|
wireSessionEvents(newSession, newAgent);
|
|
@@ -3322,6 +3555,7 @@ async function main() {
|
|
|
3322
3555
|
if (currentSession) {
|
|
3323
3556
|
const sessionToClose = currentSession;
|
|
3324
3557
|
currentSession = null;
|
|
3558
|
+
activeAgentSession = null;
|
|
3325
3559
|
// Track async close so new connections can wait for byte stream handler to be released
|
|
3326
3560
|
pendingSessionClose = (async () => {
|
|
3327
3561
|
try {
|
package/dist/recall-client.js
CHANGED
|
@@ -35,6 +35,23 @@ export class RecallClient extends EventEmitter {
|
|
|
35
35
|
// - `url` and `events` are flat on the endpoint object (NOT nested under `config`)
|
|
36
36
|
// - `transcription_options` does NOT exist — use `transcript.provider`
|
|
37
37
|
// - Both transcript.provider AND realtime_endpoints must be set, or no events delivered
|
|
38
|
+
//
|
|
39
|
+
// ARCHITECTURE (post-2026-05-22 redesign):
|
|
40
|
+
// Input (meeting → osborn): Recall's documented WebSocket audio protocol.
|
|
41
|
+
// `audio_separate_raw` config + websocket realtime endpoint streams
|
|
42
|
+
// per-participant PCM (S16LE 16kHz mono, base64 in JSON) to the agent's
|
|
43
|
+
// /meeting-audio-in WS handler. Bot's own audio is excluded by default
|
|
44
|
+
// → zero possibility of feedback loop, no echo cancellation needed.
|
|
45
|
+
// Output (osborn → meeting): webpage output_media (LiveKit-on-page). Bot
|
|
46
|
+
// page subscribes to osborn's LiveKit audio track and plays it via
|
|
47
|
+
// track.attach(); Recall captures the page's audio output and injects
|
|
48
|
+
// into the meeting.
|
|
49
|
+
// Webhook transcripts (transcript.data): retained as a SECONDARY signal —
|
|
50
|
+
// the agent index.ts handler for this event currently logs but does NOT
|
|
51
|
+
// forward to the LLM (intentionally disabled). The Deepgram WS path
|
|
52
|
+
// above is the LLM input.
|
|
53
|
+
const httpBase = webhookBaseUrl.replace(/\/$/, '');
|
|
54
|
+
const wsBase = httpBase.replace(/^https?:\/\//, m => m === 'https://' ? 'wss://' : 'ws://');
|
|
38
55
|
const res = await fetch(`${RECALL_BASE_URL}/bot`, {
|
|
39
56
|
method: 'POST',
|
|
40
57
|
headers: {
|
|
@@ -49,25 +66,39 @@ export class RecallClient extends EventEmitter {
|
|
|
49
66
|
provider: {
|
|
50
67
|
// recallai_streaming is built-in — no external API key needed,
|
|
51
68
|
// low-latency, works across all meeting platforms.
|
|
69
|
+
// Kept for the secondary webhook signal (display / future use);
|
|
70
|
+
// LLM input now comes from the Deepgram WS pipe below.
|
|
52
71
|
recallai_streaming: {
|
|
53
72
|
mode: 'prioritize_low_latency',
|
|
54
73
|
language_code: 'en',
|
|
55
74
|
},
|
|
56
75
|
},
|
|
57
76
|
},
|
|
58
|
-
|
|
77
|
+
// Per-participant raw PCM audio stream. Bot's own audio is excluded
|
|
78
|
+
// (we don't set include_bot_in_recording.audio:true).
|
|
79
|
+
audio_separate_raw: {},
|
|
80
|
+
realtime_endpoints: [
|
|
81
|
+
{
|
|
82
|
+
// Transcript webhook (secondary signal; LLM forwarding disabled).
|
|
59
83
|
type: 'webhook',
|
|
60
|
-
url: `${
|
|
84
|
+
url: `${httpBase}/webhook/recall`,
|
|
61
85
|
events: ['transcript.data'],
|
|
62
|
-
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
// Per-participant PCM audio → agent's Deepgram STT pipe.
|
|
89
|
+
type: 'websocket',
|
|
90
|
+
url: `${wsBase}/meeting-audio-in`,
|
|
91
|
+
events: ['audio_separate_raw.data'],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
63
94
|
},
|
|
64
95
|
output_media: {
|
|
65
96
|
camera: {
|
|
66
97
|
// `kind` (not `type`) — confirmed from prior debugging.
|
|
67
|
-
// The page Recall renders
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
98
|
+
// The page Recall renders connects to LiveKit and plays osborn's
|
|
99
|
+
// TTS audio via track.attach(); Recall captures the page audio.
|
|
100
|
+
// The page does NOT call getUserMedia anymore — input now comes
|
|
101
|
+
// from the audio_separate_raw WebSocket above.
|
|
71
102
|
kind: 'webpage',
|
|
72
103
|
config: {
|
|
73
104
|
url: outputPageUrl,
|