osborn 0.9.84 → 0.9.86
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 +11 -1
- package/dist/recall-client.d.ts +1 -0
- package/dist/recall-client.js +35 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -303,6 +303,12 @@ function startApiServer(workingDir, port) {
|
|
|
303
303
|
req.on('end', () => {
|
|
304
304
|
try {
|
|
305
305
|
const payload = JSON.parse(body);
|
|
306
|
+
// 0.9.86: log every receipt so we can SEE what Recall streams
|
|
307
|
+
// (event type + word count) — the handler was silent, making it
|
|
308
|
+
// impossible to tell whether webhooks arrive or are just filtered.
|
|
309
|
+
const evt = payload?.event ?? 'unknown';
|
|
310
|
+
const wc = (payload?.data?.data?.words ?? []).length;
|
|
311
|
+
console.log(`📨 Recall webhook: event=${evt} words=${wc}`);
|
|
306
312
|
const recall = getRecallClient();
|
|
307
313
|
if (recall)
|
|
308
314
|
recall.handleWebhook(payload);
|
|
@@ -4624,7 +4630,11 @@ async function main() {
|
|
|
4624
4630
|
// pipeline at all. Recall captures the meeting audio internally
|
|
4625
4631
|
// and we pull the transcript via its REST API every ~30s.
|
|
4626
4632
|
await sendToFrontend({ type: 'meeting_joining', message: 'Osborn is joining your meeting...' });
|
|
4627
|
-
|
|
4633
|
+
// 0.9.85: optional castUrl — a webpage to display as the bot's
|
|
4634
|
+
// camera (live feed / seeded site). Passed from the frontend or
|
|
4635
|
+
// env OSBORN_MEETING_CAST_URL for testing.
|
|
4636
|
+
const castUrl = data.castUrl || process.env.OSBORN_MEETING_CAST_URL || undefined;
|
|
4637
|
+
const botId = await recallJoin.joinMeeting(meetingUrl, webhookBase, { castUrl });
|
|
4628
4638
|
const sessionId = currentLLM?.sessionId || currentResumeSessionId || 'default';
|
|
4629
4639
|
recallJoin.registerBot(botId, sessionId);
|
|
4630
4640
|
activeMeetingBotId = botId;
|
package/dist/recall-client.d.ts
CHANGED
|
@@ -81,6 +81,7 @@ export declare class RecallClient extends EventEmitter {
|
|
|
81
81
|
*/
|
|
82
82
|
joinMeeting(meetingUrl: string, webhookBaseUrl: string, opts?: {
|
|
83
83
|
botName?: string;
|
|
84
|
+
castUrl?: string;
|
|
84
85
|
}): Promise<string>;
|
|
85
86
|
/**
|
|
86
87
|
* Fetch the bot's current transcript. Returns an array of "transcript turns"
|
package/dist/recall-client.js
CHANGED
|
@@ -23,6 +23,19 @@ export class RecallClient extends EventEmitter {
|
|
|
23
23
|
*/
|
|
24
24
|
async joinMeeting(meetingUrl, webhookBaseUrl, opts) {
|
|
25
25
|
const botName = opts?.botName ?? 'Osborn';
|
|
26
|
+
// 0.9.85: optional CAST — display a webpage as the bot's camera in the
|
|
27
|
+
// meeting (Recall output_media, the piece the v0.9.44 rewrite removed).
|
|
28
|
+
// Point castUrl at the session-engine live-feed viewer, a seeded site
|
|
29
|
+
// (e.g. voice-native's LinkedIn), or a research/output page. This is the
|
|
30
|
+
// "the client sees content" visual side of the copilot. Left off by
|
|
31
|
+
// default (silent, invisible observer) — only casts when castUrl is set.
|
|
32
|
+
const cast = opts?.castUrl && /^https?:\/\//.test(opts.castUrl)
|
|
33
|
+
? { output_media: { camera: { kind: 'webpage', config: { url: opts.castUrl } } } }
|
|
34
|
+
: {};
|
|
35
|
+
if (opts?.castUrl && !Object.keys(cast).length)
|
|
36
|
+
console.log(`⚠️ castUrl ignored (not http(s)): ${opts.castUrl}`);
|
|
37
|
+
else if (Object.keys(cast).length)
|
|
38
|
+
console.log(`📺 Meeting cast enabled — bot camera = ${opts.castUrl}`);
|
|
26
39
|
// 0.9.84: LIVE transcript via realtime_endpoints. The v0.9.44 rewrite
|
|
27
40
|
// removed this and left only 30s polling of the batch download_url — but
|
|
28
41
|
// that URL is a POST-PROCESSING artifact (empty until the meeting ends),
|
|
@@ -58,6 +71,7 @@ export class RecallClient extends EventEmitter {
|
|
|
58
71
|
body: JSON.stringify({
|
|
59
72
|
meeting_url: meetingUrl,
|
|
60
73
|
bot_name: botName,
|
|
74
|
+
...cast,
|
|
61
75
|
recording_config: {
|
|
62
76
|
transcript: {
|
|
63
77
|
provider: {
|
|
@@ -135,9 +149,19 @@ export class RecallClient extends EventEmitter {
|
|
|
135
149
|
const bot = (await res.json());
|
|
136
150
|
return bot.status_changes?.at(-1)?.code ?? 'unknown';
|
|
137
151
|
}
|
|
152
|
+
// 0.9.86: track the last text we emitted per speaker so partials don't
|
|
153
|
+
// spam the LLM with every incremental word — only emit when the utterance
|
|
154
|
+
// grows meaningfully or finalizes.
|
|
155
|
+
#lastEmitted = new Map();
|
|
138
156
|
handleWebhook(payload) {
|
|
139
|
-
//
|
|
140
|
-
|
|
157
|
+
// Accept BOTH finals (transcript.data) AND partials (transcript.partial_data).
|
|
158
|
+
// recallai_streaming in prioritize_low_latency mode emits partials DURING
|
|
159
|
+
// the call and finals lag — ignoring partials (the old behavior) meant no
|
|
160
|
+
// live transcript at all (confirmed 2026-07-29: 0 webhook-processed turns
|
|
161
|
+
// mid-call). We emit partials for liveness, deduped against the last text.
|
|
162
|
+
const isFinal = payload.event === 'transcript.data';
|
|
163
|
+
const isPartial = payload.event === 'transcript.partial_data';
|
|
164
|
+
if (!isFinal && !isPartial)
|
|
141
165
|
return;
|
|
142
166
|
const words = payload.data?.data?.words ?? [];
|
|
143
167
|
const text = words.map(w => w.text).join(' ').trim();
|
|
@@ -145,7 +169,15 @@ export class RecallClient extends EventEmitter {
|
|
|
145
169
|
return;
|
|
146
170
|
const speaker = payload.data?.data?.participant?.name ?? 'Unknown';
|
|
147
171
|
const botId = payload.data?.bot?.id ?? 'unknown';
|
|
148
|
-
|
|
172
|
+
const key = `${botId}:${speaker}`;
|
|
173
|
+
// Dedup: skip a partial that just repeats/prefixes what we already sent.
|
|
174
|
+
const prev = this.#lastEmitted.get(key) ?? '';
|
|
175
|
+
if (!isFinal && (text === prev || (prev && text.startsWith(prev) && text.length - prev.length < 8)))
|
|
176
|
+
return;
|
|
177
|
+
this.#lastEmitted.set(key, text);
|
|
178
|
+
if (isFinal)
|
|
179
|
+
this.#lastEmitted.delete(key); // reset for the next utterance
|
|
180
|
+
this.emit('transcript', { botId, speaker, text, partial: !isFinal });
|
|
149
181
|
}
|
|
150
182
|
registerBot(botId, sessionId) {
|
|
151
183
|
this.#activeBots.set(botId, sessionId);
|