osborn 0.9.113 → 0.9.115
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 +61 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -192,6 +192,11 @@ const livekitState = {
|
|
|
192
192
|
// connect-with-retry loop) so the module-level HTTP server can drive them.
|
|
193
193
|
let intentionalLeave = false;
|
|
194
194
|
let connectRoomHook = null;
|
|
195
|
+
// Mints a LISTEN-ONLY LiveKit token for the meeting canvas so it can join the
|
|
196
|
+
// agent's room and play the agent's REAL TTS audio track — bit-identical to
|
|
197
|
+
// the browser voice experience (2026-08-01 quality architecture; replaces the
|
|
198
|
+
// synth-file chain when connected). Set from main() where the room name lives.
|
|
199
|
+
let canvasTokenHook = null;
|
|
195
200
|
let leaveRoomHook = null;
|
|
196
201
|
// Hook for the bug-reporter skill. The /report-bug HTTP endpoint validates the
|
|
197
202
|
// payload + generates the reportId in the module-level handler, then delegates
|
|
@@ -237,7 +242,7 @@ async function synthMp3(text) {
|
|
|
237
242
|
const dgKey = process.env.DEEPGRAM_API_KEY;
|
|
238
243
|
if (dgKey) {
|
|
239
244
|
try {
|
|
240
|
-
const dg = await fetch('https://api.deepgram.com/v1/speak?model=aura-2-
|
|
245
|
+
const dg = await fetch('https://api.deepgram.com/v1/speak?model=aura-2-asteria-en&encoding=mp3&bit_rate=48000', {
|
|
241
246
|
method: 'POST',
|
|
242
247
|
headers: { 'Authorization': `Token ${dgKey}`, 'Content-Type': 'application/json' },
|
|
243
248
|
body: JSON.stringify({ text: text.slice(0, 4000) }),
|
|
@@ -342,6 +347,18 @@ function startApiServer(workingDir, port) {
|
|
|
342
347
|
}
|
|
343
348
|
return;
|
|
344
349
|
}
|
|
350
|
+
if (req.method === 'GET' && url.pathname === '/canvas-token') {
|
|
351
|
+
// Listen-only LiveKit credentials for the meeting canvas (see hook doc).
|
|
352
|
+
const out = canvasTokenHook ? await canvasTokenHook() : null;
|
|
353
|
+
if (!out) {
|
|
354
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
355
|
+
res.end(JSON.stringify({ error: 'no active room' }));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
359
|
+
res.end(JSON.stringify(out));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
345
362
|
if (req.method === 'GET' && url.pathname === '/skills') {
|
|
346
363
|
// Installed skills — same list the chat's get_skills data-channel message
|
|
347
364
|
// returns, exposed over HTTP so the DASHBOARD (no LiveKit connection) can
|
|
@@ -642,7 +659,7 @@ function startApiServer(workingDir, port) {
|
|
|
642
659
|
// WAV/linear16, not mp3: mp3 files carry encoder padding (leading/
|
|
643
660
|
// trailing silence + boundary click) — back-to-back sentence clips
|
|
644
661
|
// were heard as "cracking". WAV is gapless-safe and cheaper to decode.
|
|
645
|
-
const dg = await fetch('https://api.deepgram.com/v1/speak?model=aura-2-
|
|
662
|
+
const dg = await fetch('https://api.deepgram.com/v1/speak?model=aura-2-asteria-en&encoding=linear16&sample_rate=48000&container=wav', {
|
|
646
663
|
method: 'POST',
|
|
647
664
|
headers: { 'Authorization': `Token ${dgKey}`, 'Content-Type': 'application/json' },
|
|
648
665
|
body: JSON.stringify({ text }),
|
|
@@ -1715,6 +1732,10 @@ async function main() {
|
|
|
1715
1732
|
// REDIRECTED into the meeting canvas instead of suppressed — the regular
|
|
1716
1733
|
// voice pipeline reused with the meeting as the sink (latency fix).
|
|
1717
1734
|
let meetingAddressedUntil = 0;
|
|
1735
|
+
// True while the meeting-canvas page is connected to the LiveKit room as a
|
|
1736
|
+
// listener (browser-parity audio path). When set, meeting replies use the
|
|
1737
|
+
// NORMAL session.say pipeline — no suppression, no redirect, no synth chain.
|
|
1738
|
+
let meetingCanvasInRoom = false;
|
|
1718
1739
|
// Distinct human speakers seen in the current meeting. In a 1:1 (one human
|
|
1719
1740
|
// + the bot) EVERY utterance is addressed to the bot — no name needed
|
|
1720
1741
|
// (2026-08-01: "can you open carfax..." got observer-suppressed because
|
|
@@ -1963,10 +1984,15 @@ async function main() {
|
|
|
1963
1984
|
}
|
|
1964
1985
|
}
|
|
1965
1986
|
else {
|
|
1966
|
-
// speech_off — a natural silence boundary
|
|
1967
|
-
//
|
|
1968
|
-
|
|
1969
|
-
|
|
1987
|
+
// speech_off — a natural silence boundary = the speaker finished their
|
|
1988
|
+
// utterance. When the conversation latch is open (1:1 or interactive
|
|
1989
|
+
// mode), that boundary IS the bot's turn — flush ADDRESSED so it
|
|
1990
|
+
// replies at natural pauses, VAD-driven, no name required
|
|
1991
|
+
// (user directive 2026-08-01).
|
|
1992
|
+
if (!isBot && meetingTranscriptBuffer.length) {
|
|
1993
|
+
const latchOpen = Date.now() < meetingAddressedUntil || meetingSpeakers.size <= 1;
|
|
1994
|
+
flushMeetingBuffer(botId, latchOpen);
|
|
1995
|
+
}
|
|
1970
1996
|
}
|
|
1971
1997
|
});
|
|
1972
1998
|
}
|
|
@@ -2632,6 +2658,16 @@ async function main() {
|
|
|
2632
2658
|
// re-captures in the same room → feedback). Set by PipelineDirectLLM.chat()
|
|
2633
2659
|
// when the turn is a [MEETING —] chunk. Normal user turns are unaffected.
|
|
2634
2660
|
if (directLLM.suppressMeetingTTS) {
|
|
2661
|
+
// MEETING MODE CONDITION (user directive 2026-08-01): says route to
|
|
2662
|
+
// the NATIVE Recall sink by default — no room/Chrome relay (that path
|
|
2663
|
+
// was moderately-to-extremely choppy). The LiveKit parity relay stays
|
|
2664
|
+
// available behind OSBORN_MEETING_AUDIO=parity for future testing.
|
|
2665
|
+
if (process.env.OSBORN_MEETING_AUDIO === 'parity' && meetingCanvasInRoom && activeMeetingBotId && Date.now() < meetingAddressedUntil) {
|
|
2666
|
+
console.log(`🔊🎼 meeting reply via NATIVE session.say (canvas relays): "${data.text.slice(0, 50)}"`);
|
|
2667
|
+
markMeetingSpeaking(data.text);
|
|
2668
|
+
// no return — normal say proceeds below
|
|
2669
|
+
}
|
|
2670
|
+
else
|
|
2635
2671
|
// ADDRESSED turn → REDIRECT the normal streaming reply into the meeting
|
|
2636
2672
|
// (reuse of the regular speak path; no Bash roundtrip). Silent-observer
|
|
2637
2673
|
// turns stay suppressed as before.
|
|
@@ -3578,6 +3614,14 @@ async function main() {
|
|
|
3578
3614
|
// connect callback, not via events).
|
|
3579
3615
|
participantConnectedHandler = async (participant) => {
|
|
3580
3616
|
console.log(`\n👤 User joined: ${participant.identity}`);
|
|
3617
|
+
// The meeting canvas joining as a listener = browser-parity audio path is
|
|
3618
|
+
// LIVE: the agent's normal session.say plays through the canvas into the
|
|
3619
|
+
// meeting, so tts_say suppression/redirect must stand down.
|
|
3620
|
+
if (participant.identity === 'meeting-canvas') {
|
|
3621
|
+
meetingCanvasInRoom = true;
|
|
3622
|
+
console.log('📽️🔊 meeting-canvas joined the LiveKit room — native session.say relays to the meeting');
|
|
3623
|
+
return; // not a user; skip session setup for it
|
|
3624
|
+
}
|
|
3581
3625
|
// A user (re)arriving cancels the meeting leave-grace — the bot stays.
|
|
3582
3626
|
cancelMeetingLeaveGrace();
|
|
3583
3627
|
// A user is present — cancel any pending agent-side "alone" leave.
|
|
@@ -5481,6 +5525,17 @@ async function main() {
|
|
|
5481
5525
|
// { roomName } (which connectRoomHook resolves to). During rollout, the legacy
|
|
5482
5526
|
// GET /room-code endpoint keeps returning the last-created room NAME so old
|
|
5483
5527
|
// frontends still function.
|
|
5528
|
+
canvasTokenHook = async () => {
|
|
5529
|
+
if (!activeRoomName)
|
|
5530
|
+
return null;
|
|
5531
|
+
const t = new AccessToken(apiKey, apiSecret, {
|
|
5532
|
+
identity: 'meeting-canvas',
|
|
5533
|
+
name: 'Meeting Canvas',
|
|
5534
|
+
metadata: JSON.stringify({ type: 'canvas' }),
|
|
5535
|
+
});
|
|
5536
|
+
t.addGrant({ roomJoin: true, room: activeRoomName, canPublish: false, canSubscribe: true, canPublishData: false });
|
|
5537
|
+
return { token: await t.toJwt(), url: process.env.LIVEKIT_URL || '', room: activeRoomName };
|
|
5538
|
+
};
|
|
5484
5539
|
connectRoomHook = async () => {
|
|
5485
5540
|
intentionalLeave = false;
|
|
5486
5541
|
cancelIdleExitTimer();
|