33const fs = require ( 'fs' ) ;
44const path = require ( 'path' ) ;
55const http = require ( 'http' ) ;
6- const { execSync, spawnSync } = require ( 'child_process' ) ;
6+ const { execSync } = require ( 'child_process' ) ;
77const WebSocket = require ( 'ws' ) ;
88
99// ── Credential check ─────────────────────────────────────────────────────────
@@ -27,39 +27,77 @@ const { createApp } = require('../src/index.js');
2727const PORT = 3099 ;
2828const AUDIO_URL = 'https://dpgr.am/spacewalk.wav' ;
2929const TMP_WAV = '/tmp/twilio_test.wav' ;
30- const TMP_MULAW = '/tmp/twilio_test.mulaw' ;
31- // Twilio sends ~20 ms frames; 8 000 Hz × 1 byte/sample × 0.02 s = 160 bytes.
32- // We use 320 to match Twilio's actual observed frame size.
3330const CHUNK_SIZE = 320 ;
3431
35- // Convert a known audio file to μ-law 8 kHz using ffmpeg.
36- function ensureFfmpeg ( ) {
37- const check = spawnSync ( 'ffmpeg' , [ '-version' ] , { stdio : 'pipe' } ) ;
38- if ( check . status === 0 ) return ;
39- console . log ( 'ffmpeg not found — installing via apt-get...' ) ;
40- execSync ( 'sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg' , { stdio : 'pipe' } ) ;
32+ const LINEAR_TO_ULAW = ( ( ) => {
33+ const BIAS = 0x84 ;
34+ const CLIP = 32635 ;
35+ const table = new Int8Array ( 65536 ) ;
36+ for ( let i = - 32768 ; i < 32768 ; i ++ ) {
37+ let sample = i < 0 ? ~ i : i ;
38+ if ( sample > CLIP ) sample = CLIP ;
39+ sample += BIAS ;
40+ let exponent = 7 ;
41+ for ( let expMask = 0x4000 ; ( sample & expMask ) === 0 && exponent > 0 ; exponent -- , expMask >>= 1 ) ;
42+ const mantissa = ( sample >> ( exponent + 3 ) ) & 0x0F ;
43+ let ulawByte = ~ ( ( ( i < 0 ? 0x80 : 0 ) | ( exponent << 4 ) | mantissa ) ) & 0xFF ;
44+ table [ i & 0xFFFF ] = ulawByte ;
45+ }
46+ return table ;
47+ } ) ( ) ;
48+
49+ function wavToMulaw8k ( wavBuffer ) {
50+ let offset = 12 ;
51+ let sampleRate = 0 , bitsPerSample = 0 , numChannels = 0 , dataStart = 0 , dataSize = 0 ;
52+ while ( offset < wavBuffer . length - 8 ) {
53+ const chunkId = wavBuffer . toString ( 'ascii' , offset , offset + 4 ) ;
54+ const chunkSize = wavBuffer . readUInt32LE ( offset + 4 ) ;
55+ if ( chunkId === 'fmt ' ) {
56+ numChannels = wavBuffer . readUInt16LE ( offset + 10 ) ;
57+ sampleRate = wavBuffer . readUInt32LE ( offset + 12 ) ;
58+ bitsPerSample = wavBuffer . readUInt16LE ( offset + 22 ) ;
59+ } else if ( chunkId === 'data' ) {
60+ dataStart = offset + 8 ;
61+ dataSize = chunkSize ;
62+ break ;
63+ }
64+ offset += 8 + chunkSize ;
65+ }
66+ if ( ! dataStart ) throw new Error ( 'Invalid WAV: no data chunk' ) ;
67+
68+ const bytesPerSample = bitsPerSample / 8 ;
69+ const totalSamples = Math . floor ( dataSize / ( bytesPerSample * numChannels ) ) ;
70+ const ratio = sampleRate / 8000 ;
71+ const outLen = Math . floor ( totalSamples / ratio ) ;
72+ const out = Buffer . alloc ( outLen ) ;
73+
74+ for ( let i = 0 ; i < outLen ; i ++ ) {
75+ const srcIdx = Math . floor ( i * ratio ) ;
76+ const byteOff = dataStart + srcIdx * bytesPerSample * numChannels ;
77+ let sample ;
78+ if ( bitsPerSample === 16 ) {
79+ sample = wavBuffer . readInt16LE ( byteOff ) ;
80+ } else if ( bitsPerSample === 24 ) {
81+ sample = ( wavBuffer [ byteOff ] | ( wavBuffer [ byteOff + 1 ] << 8 ) | ( wavBuffer [ byteOff + 2 ] << 16 ) ) ;
82+ if ( sample & 0x800000 ) sample |= ~ 0xFFFFFF ;
83+ sample = sample >> 8 ;
84+ } else if ( bitsPerSample === 32 ) {
85+ sample = wavBuffer . readInt32LE ( byteOff ) >> 16 ;
86+ } else {
87+ sample = ( wavBuffer [ byteOff ] - 128 ) << 8 ;
88+ }
89+ out [ i ] = LINEAR_TO_ULAW [ sample & 0xFFFF ] ;
90+ }
91+ return out ;
4192}
4293
4394function prepareMulawAudio ( ) {
44- ensureFfmpeg ( ) ;
45-
4695 console . log ( 'Downloading test audio...' ) ;
4796 execSync ( `curl -s -L -o "${ TMP_WAV } " "${ AUDIO_URL } "` , { stdio : 'pipe' } ) ;
4897
49- console . log ( 'Converting to μ-law 8 kHz mono (ffmpeg)...' ) ;
50- const result = spawnSync ( 'ffmpeg' , [
51- '-y' , '-i' , TMP_WAV ,
52- '-ar' , '8000' , '-ac' , '1' , '-f' , 'mulaw' , TMP_MULAW ,
53- ] , { stdio : 'pipe' } ) ;
54-
55- if ( result . error ) {
56- throw new Error ( `ffmpeg could not be started: ${ result . error . message } ` ) ;
57- }
58- if ( result . status !== 0 ) {
59- throw new Error ( `ffmpeg failed (exit ${ result . status } ): ${ ( result . stderr || Buffer . alloc ( 0 ) ) . toString ( ) . slice ( 0 , 300 ) } ` ) ;
60- }
61-
62- const audio = fs . readFileSync ( TMP_MULAW ) ;
98+ console . log ( 'Converting to μ-law 8 kHz mono...' ) ;
99+ const wavData = fs . readFileSync ( TMP_WAV ) ;
100+ const audio = wavToMulaw8k ( wavData ) ;
63101 console . log ( `✓ Audio ready: ${ audio . length } bytes of μ-law 8 kHz` ) ;
64102 return audio ;
65103}
@@ -159,7 +197,6 @@ function testMediaStreamFlow(port, audioData) {
159197 if ( offset >= audioData . length || offset >= MAX_BYTES ) {
160198 // 4. "stop" — call ended
161199 ws . send ( JSON . stringify ( { event : 'stop' , streamSid : 'MZ_ci_test' } ) ) ;
162- setTimeout ( ( ) => ws . close ( ) , 500 ) ;
163200 return ;
164201 }
165202
@@ -218,17 +255,18 @@ async function run() {
218255 console . log ( `\n✓ Received ${ transcripts . length } transcript event(s)` ) ;
219256 console . log ( ` First: ${ transcripts [ 0 ] } ` ) ;
220257
221- // Verify we got recognisable English words (any transcript text counts)
258+ // Verify recognisable words from the spacewalk recording
222259 const combined = transcripts . join ( ' ' ) . toLowerCase ( ) ;
223- const hasText = combined . replace ( / \[ (?: f i n a l | i n t e r i m ) \] / g, '' ) . trim ( ) . length > 0 ;
260+ const expectedWords = [ 'spacewalk' , 'astronaut' , 'nasa' ] ;
261+ const found = expectedWords . filter ( w => combined . includes ( w ) ) ;
224262
225- if ( ! hasText ) {
263+ if ( found . length === 0 ) {
226264 throw new Error (
227- `Transcripts arrived but contained no text .\n` +
265+ `Transcripts arrived but no expected words found .\n` +
228266 `Got: ${ transcripts . slice ( 0 , 3 ) . join ( ' | ' ) } ` ,
229267 ) ;
230268 }
231- console . log ( `✓ Transcript content verified (received text from Deepgram )` ) ;
269+ console . log ( `✓ Transcript content verified (found: ${ found . join ( ', ' ) } )` ) ;
232270
233271 } finally {
234272 server . close ( ) ;
0 commit comments