@inetafrica/open-claudia 3.5.27 → 3.5.28
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/CHANGELOG.md +23 -6
- package/channels/kazee/call/manager.js +2 -137
- package/channels/kazee/call/session.js +2 -24
- package/core/browser-control.js +201 -17
- package/core/browser-drive.js +619 -0
- package/core/browser-lock.js +33 -2
- package/core/browser-view.js +90 -5
- package/core/cards.js +18 -0
- package/core/loopback.js +4 -1
- package/core/system-prompt.js +3 -11
- package/core/tool-guard.js +3 -0
- package/package.json +2 -2
- package/test-kazee-call-manager.js +0 -5
- package/test-kazee-call-session.js +0 -3
- package/test-provider-language.js +12 -5
- package/test-provider-prompt-parity.js +0 -1
- package/web.js +4 -1
- package/channels/kazee/call/quick-voice.js +0 -295
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.5.
|
|
3
|
+
## 3.5.28
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The Browser app is always on: opening it while the bot is not browsing opens the bot's browser, instead of a dead end (Sumeet, 10 Oct: "half the time i have to ask you switch on the browser"; 11 Oct 07:43 EAT network-mon's Browser showed only "isn't open right now"). A canary build for the Browser canary bot, on 3.5.26; it does not carry 3.5.27 (calls, Sumeet's own bot).
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- **
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
- **`POST /api/browser/start`** (Agent Space's control token only, like every Browser app route). With no browser open, the bot's own default session opens on a start page (`https://www.google.com/`), with the bot's own agent-browser environment and socket folder (`AGENT_BROWSER_SOCKET_DIR`, else `~/.agent-browser`), exactly as its own `agent-browser open` would, so the bot carries on in that tab. A browser already open is answered as it is (the newest session), and two people opening the app at once start one browser. `GET /api/browser/control` says `start: true`; Agent Space's front door (223b978) asks only bots that say so.
|
|
8
|
+
- **Tests:** `test-browser-control.js`, `test-web-browser-app.js`.
|
|
9
|
+
|
|
10
|
+
## 3.5.26
|
|
11
|
+
|
|
12
|
+
The Browser app's phone and web view stays sharp after a page load. A canary build for the Browser canary bot first; the fleet waits for Sumeet's OK.
|
|
13
|
+
|
|
14
|
+
- **Sharp after every page.** On the canary's Chrome 150 (Linux) the driver's pictures dropped from the view's density to 1x at the first page load (the view kept its density; the picture did not). After a main-frame navigation or a load, the window is now resized by a pixel and back, which brings the density back: 906x1760 pictures for a 412x800 phone at 2.2x on duckduckgo.com, en.m.wikipedia.org and example.com, 1 to 4 s after each load.
|
|
15
|
+
- **Tests:** `test-browser-drive.js`.
|
|
16
|
+
|
|
17
|
+
## 3.5.25
|
|
18
|
+
|
|
19
|
+
The Browser app feels like a browser on the phone and on the web (Sumeet, 10 Oct 08:53 EAT on his phone: "it feels like a browser on my phone… like Google Chrome app", and 09:18: "the web and mobile still need to also feel native… don't forget the website"). A canary build for the Browser canary bot first; the fleet waits for his OK.
|
|
20
|
+
|
|
21
|
+
- **The bot's tab takes the driver's screen.** With Chrome in its own container, a take (or a `fit` when the screen changes: a turned phone, a resized panel, a keyboard that opens) that carries the driver's screen (`viewport`: CSS size, pixel density, phone layout, touch) fits the bot's tab to it through `core/browser-drive.js`. A phone gets the page's phone layout, full screen and sharp; the web gets a desktop page at the panel's size. Handing back puts the tab back to its own size. The browser's identity (user agent) never changes, so sites signed into stay signed in after the hand back. `GET /api/browser/control` says `fit: true`.
|
|
22
|
+
- **The driver's input goes straight to Chrome,** through the view's own DevTools session, in order. Touches keep their timing, so a swipe flings; typed text is inserted whole (`input_text`, at most 500 characters: accents, emoji, the keyboard's corrections); every point is scaled by the view's density, because Chrome reads input in the drawn picture's pixels. A bot without the view takes `input_text` as key presses.
|
|
23
|
+
- **The page's text fields.** The view reads where the page's text fields are and what kind they are (never a value) and streams them as `fields` events, so the Browser page opens the phone's own keyboard of the right kind the moment a field is tapped.
|
|
24
|
+
- **Pictures.** The view streams its own screencast at the driver's size (at most 1.6 million pixels, 30 a second). `/api/browser/stream` serves it while someone drives, up to 30 fps, and keeps the newest frame while a slow viewer drains instead of dropping it, so the last picture of a scroll always arrives.
|
|
25
|
+
- **A hand back wakes the bot only when it waits for that person:** it sent them an Open Browser card (cards now record `openApp`), or one of its browser commands was refused while they drove (the tool guard marks it). Otherwise the hand back is only logged. On 9 Oct a test hand back set off a 28-minute tangent on the canary.
|
|
26
|
+
- **Correction to 3.5.24:** in container mode, sessions are not pinned to tabs (Agent Space dropped `AGENT_BROWSER_PIN_TAB` in a98f239); a session uses the tab that is open.
|
|
27
|
+
- **Tests:** `test-browser-drive.js` (new), `test-browser-control.js`, `test-web-browser-app.js`.
|
|
11
28
|
|
|
12
29
|
## 3.5.24
|
|
13
30
|
|
|
@@ -5,7 +5,6 @@ const { CallLeaseHeartbeat } = require("./lease");
|
|
|
5
5
|
const { publicCallIdentity, requestRoomAccess, supportedIceServers } = require("./access");
|
|
6
6
|
const { canonicalForChannel } = require("../../../core/identity");
|
|
7
7
|
const { KazeeCallSession } = require("./session");
|
|
8
|
-
const quickVoice = require("./quick-voice");
|
|
9
8
|
|
|
10
9
|
const DEFAULT_JANUS_URL = "wss://janus.kazee.africa/ws";
|
|
11
10
|
const DEFAULT_ICE_SERVERS = [
|
|
@@ -15,34 +14,6 @@ const DEFAULT_ICE_SERVERS = [
|
|
|
15
14
|
// FCM push TTL is 60s and chat-central treats ringing logs as fresh for 90s.
|
|
16
15
|
// This is the outbound invitation deadline; terminal server events end it early.
|
|
17
16
|
const RING_TIMEOUT_MS = 60_000;
|
|
18
|
-
const qlog = (msg) => console.log(`${new Date().toISOString()} Kazee call quick: ${msg}`);
|
|
19
|
-
// The quick line must be ready this soon after the words, or it is skipped:
|
|
20
|
-
// by then the full agent is close to speaking itself.
|
|
21
|
-
const QUICK_LINE_MAX_MS = 3500;
|
|
22
|
-
// While the full agent works: a "still on it" line after this much quiet,
|
|
23
|
-
// at most a few per turn, never after the answer.
|
|
24
|
-
const QUICK_FILLER_QUIET_MS = 8000;
|
|
25
|
-
const QUICK_FILLER_MAX = 3;
|
|
26
|
-
const QUICK_TICK_MS = 500;
|
|
27
|
-
|
|
28
|
-
// The provider the call's chat runs on: the quick voice uses its CLI and
|
|
29
|
-
// credentials, as the full agent's turns do.
|
|
30
|
-
function chatProvider({ adapter, chatId, userId }) {
|
|
31
|
-
const { runInChat } = require("../../../core/context");
|
|
32
|
-
const { conversationKeyFor, roomKeyFor } = require("../../../core/identity");
|
|
33
|
-
const transport = adapter?.type || "kazee";
|
|
34
|
-
const id = runInChat({
|
|
35
|
-
adapter, channelId: String(chatId), userId: String(userId), transport,
|
|
36
|
-
canonicalUserId: canonicalForChannel(transport, String(userId)),
|
|
37
|
-
conversationKey: conversationKeyFor(transport, String(chatId)),
|
|
38
|
-
roomKey: roomKeyFor(transport, String(chatId)),
|
|
39
|
-
}, () => {
|
|
40
|
-
const state = require("../../../core/state");
|
|
41
|
-
return state.getActiveProvider(state.currentState());
|
|
42
|
-
});
|
|
43
|
-
return id ? require("../../../core/providers").registry.getProvider(id) : null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
17
|
const OUTGOING_GREETING = "Hey, it's Claudia. Can you hear me okay?";
|
|
47
18
|
// Inbound must greet too: callers get silence until they speak first, which
|
|
48
19
|
// reads as a dead call (live test 2026-08-08 — media was up, caller hung up).
|
|
@@ -50,8 +21,7 @@ const INCOMING_GREETING = "Hey, it's Claudia — you're through. Can you hear me
|
|
|
50
21
|
|
|
51
22
|
class KazeeCallManager {
|
|
52
23
|
constructor({ adapter, SessionImpl = KazeeCallSession, janusUrl = process.env.KAZEE_JANUS_URL || DEFAULT_JANUS_URL,
|
|
53
|
-
iceServers = DEFAULT_ICE_SERVERS, deviceId = randomUUID()
|
|
54
|
-
quickSettings = quickVoice.quickVoiceSettings, quickTiming = {} } = {}) {
|
|
24
|
+
iceServers = DEFAULT_ICE_SERVERS, deviceId = randomUUID() } = {}) {
|
|
55
25
|
this.adapter = adapter;
|
|
56
26
|
this.SessionImpl = SessionImpl;
|
|
57
27
|
this.janusUrl = janusUrl;
|
|
@@ -60,10 +30,6 @@ class KazeeCallManager {
|
|
|
60
30
|
this.deviceId = deviceId;
|
|
61
31
|
this._pending = null;
|
|
62
32
|
this._ringTimer = null;
|
|
63
|
-
this.resolveQuickProvider = resolveQuickProvider;
|
|
64
|
-
this.quickSpawn = quickSpawn;
|
|
65
|
-
this.quickSettings = quickSettings;
|
|
66
|
-
this.quickTiming = { lineMaxMs: QUICK_LINE_MAX_MS, fillerQuietMs: QUICK_FILLER_QUIET_MS, fillerMax: QUICK_FILLER_MAX, tickMs: QUICK_TICK_MS, ...quickTiming };
|
|
67
33
|
}
|
|
68
34
|
|
|
69
35
|
wire(socket) {
|
|
@@ -123,7 +89,6 @@ class KazeeCallManager {
|
|
|
123
89
|
// Several terminal paths clear this.session before close() calls back.
|
|
124
90
|
// Invalidate the closed call's work even when it no longer owns the slot.
|
|
125
91
|
session?._voiceTurn?.controller.abort("call-ended");
|
|
126
|
-
this._stopQuickVoice(session);
|
|
127
92
|
if (this.session !== session) return;
|
|
128
93
|
this._detachSession(session);
|
|
129
94
|
if (this.adapter._socket?.connected) this.adapter._socket.emit("call:end", {
|
|
@@ -133,7 +98,6 @@ class KazeeCallManager {
|
|
|
133
98
|
|
|
134
99
|
_detachSession(session) {
|
|
135
100
|
session?._voiceTurn?.controller.abort("call-ended");
|
|
136
|
-
this._stopQuickVoice(session);
|
|
137
101
|
if (this.session === session) this.session = null;
|
|
138
102
|
}
|
|
139
103
|
|
|
@@ -187,14 +151,12 @@ class KazeeCallManager {
|
|
|
187
151
|
...this._sessionOptions(config, access, boundIdentity, pending.controller.signal), greeting: INCOMING_GREETING,
|
|
188
152
|
onTranscript: (text, meta) => this._transcript(text, { ...payload, ...boundIdentity }, meta, session),
|
|
189
153
|
onSpeechStart: () => this._speechStart(session),
|
|
190
|
-
onSpeechNoise: () => this._speechNoise(session),
|
|
191
154
|
onMediaReady: () => this._mediaReady(session),
|
|
192
155
|
onEnded: reason => { session?._leaseHeartbeat?.close(); pending.controller.abort(); this._sessionEnded(session, reason); },
|
|
193
156
|
});
|
|
194
157
|
session.callIdentity = boundIdentity;
|
|
195
158
|
session.chatId = chatId;
|
|
196
159
|
this.session = session;
|
|
197
|
-
this._startQuickVoice(session, chatId, callerId);
|
|
198
160
|
this._manageLease(session, access);
|
|
199
161
|
await session.start();
|
|
200
162
|
if (this.session !== session || session.closed || pending.controller.signal.aborted) return;
|
|
@@ -236,14 +198,12 @@ class KazeeCallManager {
|
|
|
236
198
|
...this._sessionOptions(config, access, identity, pending.controller.signal), asCaller: true, greeting: OUTGOING_GREETING,
|
|
237
199
|
onTranscript: (text, meta) => this._transcript(text, remote, meta, session),
|
|
238
200
|
onSpeechStart: () => this._speechStart(session),
|
|
239
|
-
onSpeechNoise: () => this._speechNoise(session),
|
|
240
201
|
onMediaReady: () => this._mediaReady(session),
|
|
241
202
|
onEnded: reason => { session?._leaseHeartbeat?.close(); pending.controller.abort(); this._clearRingTimer(); this._sessionEnded(session, reason); },
|
|
242
203
|
});
|
|
243
204
|
session.callIdentity = identity;
|
|
244
205
|
session.chatId = chat;
|
|
245
206
|
this.session = session;
|
|
246
|
-
this._startQuickVoice(session, chat, target);
|
|
247
207
|
this._manageLease(session, access);
|
|
248
208
|
await session.start();
|
|
249
209
|
if (this.session !== session || session.closed || pending.controller.signal.aborted) throw new Error("Call cancelled");
|
|
@@ -287,96 +247,6 @@ class KazeeCallManager {
|
|
|
287
247
|
// complete transcript exists; a cough must not discard an accepted ask.
|
|
288
248
|
}
|
|
289
249
|
|
|
290
|
-
// The sound that muted the turn had no words in it: not a new request, so
|
|
291
|
-
// the answer being worked on may speak again (11 Oct: wind while walking
|
|
292
|
-
// muted a lookup's answer for good, and the caller heard nothing).
|
|
293
|
-
_speechNoise(session) {
|
|
294
|
-
const turn = session?._voiceTurn;
|
|
295
|
-
if (!turn?.muted || turn.controller.signal.aborted || this.session !== session || session.closed) return;
|
|
296
|
-
turn.muted = false;
|
|
297
|
-
console.log(`${new Date().toISOString()} Kazee call noise: muted turn resumed`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ── Quick voice ───────────────────────────────────────────────────
|
|
301
|
-
|
|
302
|
-
_startQuickVoice(session, chatId, userId) {
|
|
303
|
-
let settings;
|
|
304
|
-
try { settings = this.quickSettings(); } catch (_) { return; }
|
|
305
|
-
if (!settings?.enabled) return;
|
|
306
|
-
let provider = null;
|
|
307
|
-
try { provider = this.resolveQuickProvider({ adapter: this.adapter, chatId, userId }); } catch (_) {}
|
|
308
|
-
if (!quickVoice.QuickVoiceProcess.supported(provider)) {
|
|
309
|
-
qlog(`off: provider ${provider?.id || "none"} has no warm stream`);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
const quick = quickVoice.track(new quickVoice.QuickVoiceProcess({
|
|
313
|
-
provider, settings, spawnImpl: this.quickSpawn, log: qlog,
|
|
314
|
-
systemPrompt: quickVoice.buildQuickPrompt({ ownerName: quickVoice.ownerDisplayName() }),
|
|
315
|
-
}));
|
|
316
|
-
session._quick = quick;
|
|
317
|
-
// The quick voice says the first line itself; the session's delayed cue
|
|
318
|
-
// would only double it.
|
|
319
|
-
session.acknowledgement = async () => null;
|
|
320
|
-
try { quick.start(); } catch (error) {
|
|
321
|
-
qlog(`start failed: ${error.message}`);
|
|
322
|
-
session._quick = null;
|
|
323
|
-
quick.stop().catch?.(() => {});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
_stopQuickVoice(session) {
|
|
328
|
-
const quick = session?._quick;
|
|
329
|
-
if (!quick) return;
|
|
330
|
-
session._quick = null;
|
|
331
|
-
quick.stop().catch?.(() => {});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// One utterance: a short fitting line as soon as the quick voice has it,
|
|
335
|
-
// then a "still on it" line whenever the full agent leaves the caller in
|
|
336
|
-
// silence, until its answer arrives. Never over the caller, never after
|
|
337
|
-
// the answer, never once a newer utterance took over.
|
|
338
|
-
_quickTurn(session, turn, text) {
|
|
339
|
-
const quick = session._quick;
|
|
340
|
-
const timing = this.quickTiming;
|
|
341
|
-
const live = () => !turn.muted && !turn.done && !turn.controller.signal.aborted &&
|
|
342
|
-
!session.closed && this.session === session && session._voiceTurn === turn && session._quick === quick;
|
|
343
|
-
const sayLine = async (message, label, { maxMs = Infinity, unlessSpoke = false } = {}) => {
|
|
344
|
-
const asked = Date.now();
|
|
345
|
-
const { generation, text: reply } = quick.say(message);
|
|
346
|
-
const cancel = () => quick.cancel(generation);
|
|
347
|
-
turn.controller.signal.addEventListener("abort", cancel, { once: true });
|
|
348
|
-
let line = "";
|
|
349
|
-
try { line = quickVoice.spokenLine(await reply); }
|
|
350
|
-
finally { turn.controller.signal.removeEventListener("abort", cancel); }
|
|
351
|
-
const ready = Date.now() - asked;
|
|
352
|
-
if (!line || !live() || ready > maxMs || (unlessSpoke && turn.spoke) || session.callerTalking?.()) {
|
|
353
|
-
qlog(`${label} skipped ready=${ready}ms`);
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
qlog(`${label} ready=${ready}ms chars=${line.length}`);
|
|
357
|
-
const spoken = await session.speak(line);
|
|
358
|
-
if (spoken === false || !live()) return false;
|
|
359
|
-
this._rememberSpeech(session, "assistant", line);
|
|
360
|
-
return true;
|
|
361
|
-
};
|
|
362
|
-
(async () => {
|
|
363
|
-
await sayLine(quickVoice.utteranceMessage(text), "line", { maxMs: timing.lineMaxMs, unlessSpoke: true });
|
|
364
|
-
let fillers = 0;
|
|
365
|
-
let lastFillerAt = Date.now();
|
|
366
|
-
while (fillers < timing.fillerMax && live()) {
|
|
367
|
-
await new Promise((resolve) => { const timer = setTimeout(resolve, timing.tickMs); timer.unref?.(); });
|
|
368
|
-
if (!live()) break;
|
|
369
|
-
if (session.speechBusy?.() || session.callerTalking?.()) continue;
|
|
370
|
-
const quietSince = Math.max(turn.startedAt, lastFillerAt, session._lastSpokeAt || 0);
|
|
371
|
-
if (Date.now() - quietSince < timing.fillerQuietMs) continue;
|
|
372
|
-
fillers += 1;
|
|
373
|
-
lastFillerAt = Date.now();
|
|
374
|
-
await sayLine(quickVoice.fillerMessage(text, (Date.now() - turn.startedAt) / 1000), `filler ${fillers}`);
|
|
375
|
-
lastFillerAt = Date.now();
|
|
376
|
-
}
|
|
377
|
-
})().catch((error) => qlog(`turn failed: ${error.message}`));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
250
|
_rememberSpeech(session, role, text) {
|
|
381
251
|
const history = session._voiceHistory ||= [];
|
|
382
252
|
if (role === "assistant" && history.at(-1)?.role === "assistant") {
|
|
@@ -392,7 +262,7 @@ class KazeeCallManager {
|
|
|
392
262
|
const speaker = String(meta.display || "").trim();
|
|
393
263
|
const attributed = (meta.participants || 0) > 1 && speaker ? `${speaker}: ${text}` : text;
|
|
394
264
|
session._voiceTurn?.controller.abort("superseded");
|
|
395
|
-
const turn = { controller: new AbortController(), muted: false
|
|
265
|
+
const turn = { controller: new AbortController(), muted: false };
|
|
396
266
|
session._voiceTurn = turn;
|
|
397
267
|
const callAdapter = Object.create(this.adapter);
|
|
398
268
|
callAdapter.callHistory = (session._voiceHistory || []).map(entry => ({ ...entry }));
|
|
@@ -407,8 +277,6 @@ class KazeeCallManager {
|
|
|
407
277
|
callAdapter.suppressChatVisibility = true;
|
|
408
278
|
// Prompt builder switches to phone-call brevity when it sees this.
|
|
409
279
|
callAdapter.liveCall = true;
|
|
410
|
-
// The quick voice says the first line; the prompt then skips its own.
|
|
411
|
-
callAdapter.quickVoice = !!session._quick?.ready();
|
|
412
280
|
const spokenSentences = [];
|
|
413
281
|
callAdapter.isCallTurnActive = () => !turn.muted && !turn.controller.signal.aborted &&
|
|
414
282
|
!session.closed && this.session === session && session._voiceTurn === turn;
|
|
@@ -419,7 +287,6 @@ class KazeeCallManager {
|
|
|
419
287
|
if (!callAdapter.isCallTurnActive()) return false;
|
|
420
288
|
const spoken = await session.speak(response);
|
|
421
289
|
if (spoken !== false && callAdapter.isCallTurnActive()) {
|
|
422
|
-
turn.spoke = true;
|
|
423
290
|
this._rememberSpeech(session, "assistant", response);
|
|
424
291
|
return true;
|
|
425
292
|
}
|
|
@@ -431,7 +298,6 @@ class KazeeCallManager {
|
|
|
431
298
|
return spoken;
|
|
432
299
|
};
|
|
433
300
|
callAdapter.send = async (_channelId, response) => {
|
|
434
|
-
turn.done = true;
|
|
435
301
|
const remainder = require("../../../core/call-speech").unspokenSuffix(response, spokenSentences);
|
|
436
302
|
if (remainder) await speak(remainder);
|
|
437
303
|
return true;
|
|
@@ -442,7 +308,6 @@ class KazeeCallManager {
|
|
|
442
308
|
console.warn(`Kazee call ignored file voice response ${voicePath}; streamed TTS is active`);
|
|
443
309
|
return true;
|
|
444
310
|
};
|
|
445
|
-
if (callAdapter.quickVoice) this._quickTurn(session, turn, attributed);
|
|
446
311
|
this.adapter._emit("message", {
|
|
447
312
|
adapter: callAdapter,
|
|
448
313
|
channelId: String(incoming.chatId),
|
|
@@ -54,7 +54,7 @@ const ACK_DELAY_MS = 2800;
|
|
|
54
54
|
class KazeeCallSession {
|
|
55
55
|
constructor({ roomId, chatId, callerId, botUserId, janusUrl, janusToken, janusTokenExpiresAt, roomToken, roomTokenExpiresAt, managedRoom = false, refreshRoomAccess, iceServers = [], iceCredentialsExpiresAt, onTranscript, onEnded, onMediaReady,
|
|
56
56
|
maxCallMs, idleMs, finalWaitMs, synthesize = synthesizePcm, synthesizeStream = null, asCaller = false, greeting = null,
|
|
57
|
-
turnDecision = detectTurn, acknowledgement = selectAcknowledgement, onSpeechStart,
|
|
57
|
+
turnDecision = detectTurn, acknowledgement = selectAcknowledgement, onSpeechStart,
|
|
58
58
|
JanusClientImpl = JanusClient, PeerConnectionImpl = RTCPeerConnection, SpeechStreamImpl = SpeechStream } = {}) {
|
|
59
59
|
this.roomId = Number(roomId);
|
|
60
60
|
this.chatId = String(chatId);
|
|
@@ -83,7 +83,6 @@ class KazeeCallSession {
|
|
|
83
83
|
this._transportExpiryTimer = null;
|
|
84
84
|
this.onTranscript = onTranscript || (() => {});
|
|
85
85
|
this.onSpeechStart = onSpeechStart || (() => {});
|
|
86
|
-
this.onSpeechNoise = onSpeechNoise || (() => {});
|
|
87
86
|
this.turnDecision = turnDecision;
|
|
88
87
|
this.acknowledgement = acknowledgement;
|
|
89
88
|
this._cueController = null;
|
|
@@ -586,7 +585,6 @@ class KazeeCallSession {
|
|
|
586
585
|
if (countWords(trimmed) < MIN_DISPATCH_WORDS) {
|
|
587
586
|
clog(`Kazee call noise turn dropped: feed=${feedId} words=${countWords(trimmed)}`);
|
|
588
587
|
if (!this._speakActive && !this._speakQueue.length) this._stopThinking();
|
|
589
|
-
this._speechWasNoise(feedId);
|
|
590
588
|
return;
|
|
591
589
|
}
|
|
592
590
|
}
|
|
@@ -595,7 +593,7 @@ class KazeeCallSession {
|
|
|
595
593
|
// Empty finalization (noise commit): nothing dispatches, no reply is
|
|
596
594
|
// coming — don't tick into the void.
|
|
597
595
|
else if (!this._speakActive && !this._speakQueue.length) this._stopThinking();
|
|
598
|
-
if (!trimmed)
|
|
596
|
+
if (!trimmed) return;
|
|
599
597
|
this.onTranscript(text, {
|
|
600
598
|
feedId,
|
|
601
599
|
display: sub?.display || "",
|
|
@@ -604,16 +602,6 @@ class KazeeCallSession {
|
|
|
604
602
|
});
|
|
605
603
|
}
|
|
606
604
|
|
|
607
|
-
// Speech start muted the turn being worked on, in case it was a new ask. A
|
|
608
|
-
// sound the recogniser found no words in (wind, footsteps, a cough) was not:
|
|
609
|
-
// unless someone is still talking or another segment is pending, the
|
|
610
|
-
// muted turn gets its voice back.
|
|
611
|
-
_speechWasNoise(feedId) {
|
|
612
|
-
if ([...this.subscribers.values()].some(peer => peer?.vad?.active || peer?.turnGate?.pending ||
|
|
613
|
-
peer?.awaitingWordsSince || peer?.segments?.size)) return;
|
|
614
|
-
this.onSpeechNoise({ feedId });
|
|
615
|
-
}
|
|
616
|
-
|
|
617
605
|
_reportMediaReady() {
|
|
618
606
|
if (this.closed || this._reportedMediaReady || !this._sentRtp || !this._receivedRtp) return;
|
|
619
607
|
this._reportedMediaReady = true;
|
|
@@ -763,16 +751,6 @@ class KazeeCallSession {
|
|
|
763
751
|
sub.stt.audio(pcm16).catch((error) => console.error("Kazee call STT send:", error.message));
|
|
764
752
|
}
|
|
765
753
|
|
|
766
|
-
// For the quick voice: someone is talking (or a turn is being decided), and
|
|
767
|
-
// speech is playing or queued.
|
|
768
|
-
callerTalking() {
|
|
769
|
-
return [...this.subscribers.values()].some(sub => sub?.vad?.active || sub?.turnGate?.pending || sub?.awaitingWordsSince);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
speechBusy() {
|
|
773
|
-
return !!(this.speaking || this._speakActive || this._speakQueue.length);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
754
|
speak(text) {
|
|
777
755
|
if (this.closed || !String(text || "").trim()) return Promise.resolve(false);
|
|
778
756
|
return new Promise((resolve, reject) => {
|