claude-phone-local 2.3.0 → 2.3.1
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/package.json +1 -1
- package/voice-app/lib/sip-handler.js +48 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-phone-local",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Local/offline fork of NetworkChuck's claude-phone: talk to Claude Code over 3CX/SIP with faster-whisper STT + Piper TTS in one Docker container.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -263,33 +263,6 @@ async function conversationLoop(endpoint, dialog, callUuid, options, deviceConfi
|
|
|
263
263
|
? "Hello! I'm " + deviceConfig.name + ". How can I help you today?"
|
|
264
264
|
: "Hello! I'm your server. How can I help you today?";
|
|
265
265
|
|
|
266
|
-
// Play a clip, then go straight into listening - no beep, no separate
|
|
267
|
-
// "ready" phase. The caller can start talking the instant she stops (or
|
|
268
|
-
// interrupt her mid-clip; playInterruptible already supports that and
|
|
269
|
-
// capture only turns on inside audio-fork the moment barge-in actually
|
|
270
|
-
// fires, so her own voice mixed into the mono fork is never mistaken for
|
|
271
|
-
// speech). This replaces the old push-to-talk-after-tone flow, where
|
|
272
|
-
// capture was only ever enabled during one narrow beep-gated window and
|
|
273
|
-
// every other second of the call - her talking, hold music, the dead gap
|
|
274
|
-
// right after she finished - was silence the caller could not be heard in.
|
|
275
|
-
async function speakThenListen(url, { timeoutMs = 30000 } = {}) {
|
|
276
|
-
const barged = await playInterruptible(endpoint, session, url);
|
|
277
|
-
if (barged) return barged;
|
|
278
|
-
|
|
279
|
-
session.setCaptureEnabled(true);
|
|
280
|
-
console.log('[' + new Date().toISOString() + '] LISTEN Waiting for speech...');
|
|
281
|
-
try {
|
|
282
|
-
const utterance = await session.waitForUtterance({ timeoutMs });
|
|
283
|
-
console.log('[' + new Date().toISOString() + '] LISTEN Got: ' + utterance.audio.length + ' bytes');
|
|
284
|
-
return utterance;
|
|
285
|
-
} catch (err) {
|
|
286
|
-
console.log('[' + new Date().toISOString() + '] LISTEN Timeout: ' + err.message);
|
|
287
|
-
return null;
|
|
288
|
-
} finally {
|
|
289
|
-
session.setCaptureEnabled(false);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
266
|
try {
|
|
294
267
|
console.log('[' + new Date().toISOString() + '] CONVERSATION Starting (session: ' + callUuid + ', device: ' + deviceName + ', voice: ' + voiceId + ')...');
|
|
295
268
|
|
|
@@ -323,40 +296,55 @@ async function conversationLoop(endpoint, dialog, callUuid, options, deviceConfi
|
|
|
323
296
|
let turnCount = 0;
|
|
324
297
|
const MAX_TURNS = 20;
|
|
325
298
|
// Speech captured by a barge-in during the previous turn's playback -
|
|
326
|
-
// consumed as this turn's input directly instead of
|
|
327
|
-
// window, which would otherwise make the caller repeat
|
|
328
|
-
// every interruption.
|
|
299
|
+
// consumed as this turn's input directly instead of a fresh ready-beep
|
|
300
|
+
// and listen window, which would otherwise make the caller repeat
|
|
301
|
+
// themselves after every interruption.
|
|
329
302
|
let pendingUtterance = null;
|
|
330
303
|
|
|
331
|
-
// First listen of the call - straight after the greeting, no beep.
|
|
332
|
-
session.setCaptureEnabled(true);
|
|
333
|
-
console.log('[' + new Date().toISOString() + '] LISTEN Waiting for speech...');
|
|
334
|
-
let utterance = null;
|
|
335
|
-
try {
|
|
336
|
-
utterance = await session.waitForUtterance({ timeoutMs: 30000 });
|
|
337
|
-
console.log('[' + new Date().toISOString() + '] LISTEN Got: ' + utterance.audio.length + ' bytes');
|
|
338
|
-
} catch (err) {
|
|
339
|
-
console.log('[' + new Date().toISOString() + '] LISTEN Timeout: ' + err.message);
|
|
340
|
-
} finally {
|
|
341
|
-
session.setCaptureEnabled(false);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
304
|
while (turnCount < MAX_TURNS) {
|
|
345
305
|
turnCount++;
|
|
346
306
|
console.log('[' + new Date().toISOString() + '] CONVERSATION Turn ' + turnCount + '/' + MAX_TURNS);
|
|
347
307
|
|
|
308
|
+
let utterance = null;
|
|
309
|
+
|
|
348
310
|
if (pendingUtterance) {
|
|
349
311
|
console.log('[' + new Date().toISOString() + '] LISTEN Using speech captured during barge-in: ' + pendingUtterance.audio.length + ' bytes');
|
|
350
312
|
utterance = pendingUtterance;
|
|
351
313
|
pendingUtterance = null;
|
|
314
|
+
} else {
|
|
315
|
+
// READY BEEP
|
|
316
|
+
try {
|
|
317
|
+
await endpoint.play(READY_BEEP_URL);
|
|
318
|
+
} catch (e) {
|
|
319
|
+
console.log('[' + new Date().toISOString() + '] BEEP: Ready beep failed, continuing');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
session.setCaptureEnabled(true);
|
|
323
|
+
console.log('[' + new Date().toISOString() + '] LISTEN Waiting for speech...');
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
utterance = await session.waitForUtterance({ timeoutMs: 30000 });
|
|
327
|
+
console.log('[' + new Date().toISOString() + '] LISTEN Got: ' + utterance.audio.length + ' bytes');
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.log('[' + new Date().toISOString() + '] LISTEN Timeout: ' + err.message);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
session.setCaptureEnabled(false);
|
|
352
333
|
}
|
|
353
334
|
|
|
354
335
|
if (!utterance) {
|
|
355
336
|
const promptUrl = await ttsService.generateSpeech("I didn't hear anything. Are you still there?", turnVoice);
|
|
356
|
-
|
|
337
|
+
await endpoint.play(promptUrl);
|
|
357
338
|
continue;
|
|
358
339
|
}
|
|
359
340
|
|
|
341
|
+
// GOT-IT BEEP
|
|
342
|
+
try {
|
|
343
|
+
await endpoint.play(GOTIT_BEEP_URL);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.log('[' + new Date().toISOString() + '] BEEP: Got-it beep failed, continuing');
|
|
346
|
+
}
|
|
347
|
+
|
|
360
348
|
// Transcribe (language auto-detected unless STT_LANGUAGE pins one)
|
|
361
349
|
const sttResult = await whisperClient.transcribeDetailed(utterance.audio, {
|
|
362
350
|
format: 'pcm',
|
|
@@ -379,7 +367,7 @@ async function conversationLoop(endpoint, dialog, callUuid, options, deviceConfi
|
|
|
379
367
|
|
|
380
368
|
if (!transcript || transcript.trim().length < 2) {
|
|
381
369
|
const clarifyUrl = await ttsService.generateSpeech("Sorry, I didn't catch that. Could you repeat?", turnVoice);
|
|
382
|
-
|
|
370
|
+
await endpoint.play(clarifyUrl);
|
|
383
371
|
continue;
|
|
384
372
|
}
|
|
385
373
|
|
|
@@ -450,35 +438,30 @@ async function conversationLoop(endpoint, dialog, callUuid, options, deviceConfi
|
|
|
450
438
|
|
|
451
439
|
console.log('[' + new Date().toISOString() + '] CLAUDE Response received');
|
|
452
440
|
|
|
453
|
-
utterance = null;
|
|
454
|
-
|
|
455
441
|
if (pendingUtterance) {
|
|
456
442
|
// Caller already interrupted during the wait and started saying
|
|
457
443
|
// something new - the answer to their original question is now
|
|
458
|
-
// moot. Skip playing it and
|
|
459
|
-
//
|
|
444
|
+
// moot. Skip playing it and let the next loop iteration process
|
|
445
|
+
// what they're actually saying now, instead of talking over/past it.
|
|
460
446
|
console.log('[' + new Date().toISOString() + '] VOICE: skipped (caller already interrupted with a new utterance)');
|
|
461
|
-
utterance = pendingUtterance;
|
|
462
|
-
pendingUtterance = null;
|
|
463
|
-
} else if (hasEndCallMarker(claudeResponse)) {
|
|
464
|
-
// Claude judged this a real sign-off from the caller's own words
|
|
465
|
-
// (not just isGoodbye()'s fixed phrase match) - play her answer
|
|
466
|
-
// (which already includes the goodbye) without listening again, then
|
|
467
|
-
// end the call.
|
|
468
|
-
const voiceLine = extractVoiceLine(claudeResponse);
|
|
469
|
-
console.log('[' + new Date().toISOString() + '] VOICE: "' + voiceLine + '"');
|
|
470
|
-
const responseUrl = await ttsService.generateSpeech(voiceLine, turnVoice);
|
|
471
|
-
await endpoint.play(responseUrl);
|
|
472
|
-
console.log('[' + new Date().toISOString() + '] CONVERSATION Claude signaled end of call');
|
|
473
|
-
break;
|
|
474
447
|
} else {
|
|
475
|
-
// Extract and play voice line with device voice
|
|
476
|
-
// into listening for the reply - no beep, no gap.
|
|
448
|
+
// Extract and play voice line with device voice
|
|
477
449
|
const voiceLine = extractVoiceLine(claudeResponse);
|
|
478
450
|
console.log('[' + new Date().toISOString() + '] VOICE: "' + voiceLine + '"');
|
|
479
451
|
|
|
480
452
|
const responseUrl = await ttsService.generateSpeech(voiceLine, turnVoice);
|
|
481
|
-
|
|
453
|
+
// Long answers are the usual thing people want to interrupt.
|
|
454
|
+
const responseBarge = await playInterruptible(endpoint, session, responseUrl);
|
|
455
|
+
if (responseBarge) {
|
|
456
|
+
pendingUtterance = responseBarge;
|
|
457
|
+
} else if (hasEndCallMarker(claudeResponse)) {
|
|
458
|
+
// Claude judged this a real sign-off from the caller's own words
|
|
459
|
+
// (not just isGoodbye()'s fixed phrase match) - she already said
|
|
460
|
+
// her goodbye as part of the normal response above, so just end
|
|
461
|
+
// the call rather than waiting for another turn.
|
|
462
|
+
console.log('[' + new Date().toISOString() + '] CONVERSATION Claude signaled end of call');
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
482
465
|
}
|
|
483
466
|
|
|
484
467
|
console.log('[' + new Date().toISOString() + '] CONVERSATION Turn ' + turnCount + ' complete');
|