@@ -3,8 +3,11 @@ import { join, dirname, resolve } from "path";
33import { fileURLToPath } from "url" ;
44import { execSync } from "child_process" ;
55import * as readline from "readline" ;
6+ import { createRequire } from "module" ;
67import type { Environment , StateFile } from "./types.ts" ;
78
9+ const require = createRequire ( import . meta. url ) ;
10+
811// ─────────────────────────────────────────────────────────────────────────────
912// Configuration
1013// ─────────────────────────────────────────────────────────────────────────────
@@ -205,7 +208,7 @@ async function checkMicrophonePermission(): Promise<boolean> {
205208 " If prompted, please grant microphone permission in System Preferences." ,
206209 ) ;
207210 console . log (
208- " System Preferences > Security & Privacy > Privacy > Microphone\n" ,
211+ " System Settings > Privacy & Security > Microphone\n" ,
209212 ) ;
210213
211214 // Ask user to continue anyway
@@ -269,7 +272,7 @@ function loadState(env: Environment): StateFile {
269272 if ( ! existsSync ( stateFilePath ) ) {
270273 console . error ( `❌ State file not found: .vapi-state.${ env } .json` ) ;
271274 console . error (
272- " Run 'npm run apply: " + env + "' first to create resources" ,
275+ " Run 'npm run apply -- " + env + "' first to create resources" ,
273276 ) ;
274277 process . exit ( 1 ) ;
275278 }
@@ -468,11 +471,21 @@ async function connectWebSocket(
468471 }
469472 } ;
470473
471- ws . onmessage = ( event ) => {
472- if ( event . data instanceof Buffer || event . data instanceof ArrayBuffer ) {
473- // Binary audio data from assistant
474+ ws . onmessage = async ( event ) => {
475+ const data = event . data ;
476+ // Binary audio data from assistant
477+ if ( data instanceof Buffer || data instanceof ArrayBuffer ) {
478+ if ( audioContext ) {
479+ audioContext . playAudio ( data ) ;
480+ }
481+ } else if ( typeof Blob !== "undefined" && data instanceof Blob ) {
482+ if ( audioContext ) {
483+ const arrayBuffer = await data . arrayBuffer ( ) ;
484+ audioContext . playAudio ( arrayBuffer ) ;
485+ }
486+ } else if ( ArrayBuffer . isView ( data ) ) {
474487 if ( audioContext ) {
475- audioContext . playAudio ( event . data ) ;
488+ audioContext . playAudio ( data . buffer as ArrayBuffer ) ;
476489 }
477490 } else {
478491 // Control message (JSON)
@@ -539,7 +552,20 @@ function handleControlMessage(
539552 }
540553 case "call-ended" : {
541554 const cm = message as CallEndedMessage ;
542- console . log ( `\n📞 Call ended: ${ cm . reason || "unknown reason" } ` ) ;
555+ const reasonLabels : Record < string , string > = {
556+ "silence-timed-out" : "Silence timeout (no speech detected)" ,
557+ "assistant-ended-call" : "Assistant ended the call" ,
558+ "customer-ended-call" : "Customer ended the call" ,
559+ "max-duration-reached" : "Maximum call duration reached" ,
560+ "assistant-error" : "Assistant error" ,
561+ "pipeline-error" : "Pipeline error" ,
562+ "voicemail-reached" : "Voicemail detected" ,
563+ "customer-did-not-answer" : "No answer" ,
564+ "assistant-request-returned-error" : "Assistant request error" ,
565+ "assistant-not-found" : "Assistant not found" ,
566+ } ;
567+ const label = cm . reason ? ( reasonLabels [ cm . reason ] ?? cm . reason ) : "unknown reason" ;
568+ console . log ( `\n📞 Call ended: ${ label } ` ) ;
543569 break ;
544570 }
545571 default :
@@ -584,23 +610,27 @@ function createAudioContext(): {
584610 playAudio : ( data : Buffer | ArrayBuffer ) => void ;
585611 close : ( ) => void ;
586612} {
587- // Lazy load speaker module
588613 let Speaker : SpeakerConstructor | null = null ;
589614 let speakerInstance : SpeakerInstance | null = null ;
590615
591616 try {
592- // Dynamic import for optional dependency
593617 Speaker = require ( "speaker" ) as SpeakerConstructor ;
594618 speakerInstance = new Speaker ! ( {
595619 channels : 1 ,
596620 bitDepth : 16 ,
597621 sampleRate : 16000 ,
598622 } ) ;
599- } catch {
600- console . warn (
601- "⚠️ 'speaker' module not installed. Audio playback disabled." ,
602- ) ;
603- console . warn ( " Install with: npm install speaker" ) ;
623+ } catch ( error ) {
624+ const msg = error instanceof Error ? error . message : String ( error ) ;
625+ if ( msg . includes ( "Cannot find module" ) ) {
626+ console . warn ( "⚠️ 'speaker' module not installed. Audio playback disabled." ) ;
627+ console . warn ( " Install with: npm install speaker" ) ;
628+ } else if ( msg . includes ( "Could not locate the bindings file" ) || msg . includes ( "NODE_MODULE_VERSION" ) ) {
629+ console . warn ( "⚠️ 'speaker' native bindings not built for this Node version." ) ;
630+ console . warn ( " Rebuild with: npm rebuild speaker" ) ;
631+ } else {
632+ console . warn ( `⚠️ Could not initialize speaker: ${ msg } ` ) ;
633+ }
604634 }
605635
606636 return {
@@ -647,9 +677,16 @@ function createMicrophoneStream(onData: (data: Buffer) => void): {
647677
648678 micInstance ! . start ( ) ;
649679 } catch ( error ) {
650- console . warn ( "⚠️ 'mic' module not installed or microphone unavailable." ) ;
651- console . warn ( " Install with: npm install mic" ) ;
652- console . warn ( " Error:" , error ) ;
680+ const msg = error instanceof Error ? error . message : String ( error ) ;
681+ if ( msg . includes ( "Cannot find module" ) ) {
682+ console . warn ( "⚠️ 'mic' module not installed. Microphone input disabled." ) ;
683+ console . warn ( " Install with: npm install mic" ) ;
684+ } else if ( msg . includes ( "sox" ) || msg . includes ( "rec" ) ) {
685+ console . warn ( "⚠️ sox/rec not found. Required for microphone input." ) ;
686+ console . warn ( " Install with: brew install sox (macOS) or apt install sox (Linux)" ) ;
687+ } else {
688+ console . warn ( `⚠️ Could not initialize microphone: ${ msg } ` ) ;
689+ }
653690 }
654691
655692 return {
0 commit comments