From 65e17af2b9512233dc68d1e2cd0c50348bbe8a4b Mon Sep 17 00:00:00 2001 From: examples-bot Date: Mon, 30 Mar 2026 12:18:52 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(020-twilio-media-streams-node):=20repla?= =?UTF-8?q?ce=20ffmpeg=20with=20pure=20Node.js=20=CE=BC-law=20conversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ffmpeg was removed from the GitHub Actions ubuntu-24.04 runner image (version 20260323.65), causing the test to crash with "Cannot read properties of null (reading 'toString')" when spawnSync returns {status: null, stderr: null} for a missing binary. Replace ffmpeg-based WAV→μ-law conversion with a pure Node.js implementation (ITU-T G.711 lookup table + WAV chunk parser). Also fix SDK v5 boolean options to use strings per migration guide. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../src/index.js | 4 +- .../tests/test.js | 102 ++++++++++++------ 2 files changed, 72 insertions(+), 34 deletions(-) diff --git a/examples/020-twilio-media-streams-node/src/index.js b/examples/020-twilio-media-streams-node/src/index.js index 1c63f0f..7ab0d0f 100644 --- a/examples/020-twilio-media-streams-node/src/index.js +++ b/examples/020-twilio-media-streams-node/src/index.js @@ -14,8 +14,8 @@ const DEEPGRAM_LIVE_OPTIONS = { encoding: 'mulaw', sample_rate: 8000, channels: 1, - smart_format: true, - interim_results: true, + smart_format: 'true', + interim_results: 'true', utterance_end_ms: 1000, }; diff --git a/examples/020-twilio-media-streams-node/tests/test.js b/examples/020-twilio-media-streams-node/tests/test.js index 22ddcb5..1a33f42 100644 --- a/examples/020-twilio-media-streams-node/tests/test.js +++ b/examples/020-twilio-media-streams-node/tests/test.js @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); const http = require('http'); -const { execSync, spawnSync } = require('child_process'); +const { execSync } = require('child_process'); const WebSocket = require('ws'); // ── Credential check ───────────────────────────────────────────────────────── @@ -27,39 +27,77 @@ const { createApp } = require('../src/index.js'); const PORT = 3099; const AUDIO_URL = 'https://dpgr.am/spacewalk.wav'; const TMP_WAV = '/tmp/twilio_test.wav'; -const TMP_MULAW = '/tmp/twilio_test.mulaw'; -// Twilio sends ~20 ms frames; 8 000 Hz × 1 byte/sample × 0.02 s = 160 bytes. -// We use 320 to match Twilio's actual observed frame size. const CHUNK_SIZE = 320; -// Convert a known audio file to μ-law 8 kHz using ffmpeg. -function ensureFfmpeg() { - const check = spawnSync('ffmpeg', ['-version'], { stdio: 'pipe' }); - if (check.status === 0) return; - console.log('ffmpeg not found — installing via apt-get...'); - execSync('sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg', { stdio: 'pipe' }); +const LINEAR_TO_ULAW = (() => { + const BIAS = 0x84; + const CLIP = 32635; + const table = new Int8Array(65536); + for (let i = -32768; i < 32768; i++) { + let sample = i < 0 ? ~i : i; + if (sample > CLIP) sample = CLIP; + sample += BIAS; + let exponent = 7; + for (let expMask = 0x4000; (sample & expMask) === 0 && exponent > 0; exponent--, expMask >>= 1); + const mantissa = (sample >> (exponent + 3)) & 0x0F; + let ulawByte = ~(((i < 0 ? 0x80 : 0) | (exponent << 4) | mantissa)) & 0xFF; + table[i & 0xFFFF] = ulawByte; + } + return table; +})(); + +function wavToMulaw8k(wavBuffer) { + let offset = 12; + let sampleRate = 0, bitsPerSample = 0, numChannels = 0, dataStart = 0, dataSize = 0; + while (offset < wavBuffer.length - 8) { + const chunkId = wavBuffer.toString('ascii', offset, offset + 4); + const chunkSize = wavBuffer.readUInt32LE(offset + 4); + if (chunkId === 'fmt ') { + numChannels = wavBuffer.readUInt16LE(offset + 10); + sampleRate = wavBuffer.readUInt32LE(offset + 12); + bitsPerSample = wavBuffer.readUInt16LE(offset + 22); + } else if (chunkId === 'data') { + dataStart = offset + 8; + dataSize = chunkSize; + break; + } + offset += 8 + chunkSize; + } + if (!dataStart) throw new Error('Invalid WAV: no data chunk'); + + const bytesPerSample = bitsPerSample / 8; + const totalSamples = Math.floor(dataSize / (bytesPerSample * numChannels)); + const ratio = sampleRate / 8000; + const outLen = Math.floor(totalSamples / ratio); + const out = Buffer.alloc(outLen); + + for (let i = 0; i < outLen; i++) { + const srcIdx = Math.floor(i * ratio); + const byteOff = dataStart + srcIdx * bytesPerSample * numChannels; + let sample; + if (bitsPerSample === 16) { + sample = wavBuffer.readInt16LE(byteOff); + } else if (bitsPerSample === 24) { + sample = (wavBuffer[byteOff] | (wavBuffer[byteOff + 1] << 8) | (wavBuffer[byteOff + 2] << 16)); + if (sample & 0x800000) sample |= ~0xFFFFFF; + sample = sample >> 8; + } else if (bitsPerSample === 32) { + sample = wavBuffer.readInt32LE(byteOff) >> 16; + } else { + sample = (wavBuffer[byteOff] - 128) << 8; + } + out[i] = LINEAR_TO_ULAW[sample & 0xFFFF]; + } + return out; } function prepareMulawAudio() { - ensureFfmpeg(); - console.log('Downloading test audio...'); execSync(`curl -s -L -o "${TMP_WAV}" "${AUDIO_URL}"`, { stdio: 'pipe' }); - console.log('Converting to μ-law 8 kHz mono (ffmpeg)...'); - const result = spawnSync('ffmpeg', [ - '-y', '-i', TMP_WAV, - '-ar', '8000', '-ac', '1', '-f', 'mulaw', TMP_MULAW, - ], { stdio: 'pipe' }); - - if (result.error) { - throw new Error(`ffmpeg could not be started: ${result.error.message}`); - } - if (result.status !== 0) { - throw new Error(`ffmpeg failed (exit ${result.status}): ${(result.stderr || Buffer.alloc(0)).toString().slice(0, 300)}`); - } - - const audio = fs.readFileSync(TMP_MULAW); + console.log('Converting to μ-law 8 kHz mono...'); + const wavData = fs.readFileSync(TMP_WAV); + const audio = wavToMulaw8k(wavData); console.log(`✓ Audio ready: ${audio.length} bytes of μ-law 8 kHz`); return audio; } @@ -159,7 +197,6 @@ function testMediaStreamFlow(port, audioData) { if (offset >= audioData.length || offset >= MAX_BYTES) { // 4. "stop" — call ended ws.send(JSON.stringify({ event: 'stop', streamSid: 'MZ_ci_test' })); - setTimeout(() => ws.close(), 500); return; } @@ -218,17 +255,18 @@ async function run() { console.log(`\n✓ Received ${transcripts.length} transcript event(s)`); console.log(` First: ${transcripts[0]}`); - // Verify we got recognisable English words (any transcript text counts) + // Verify recognisable words from the spacewalk recording const combined = transcripts.join(' ').toLowerCase(); - const hasText = combined.replace(/\[(?:final|interim)\]/g, '').trim().length > 0; + const expectedWords = ['spacewalk', 'astronaut', 'nasa']; + const found = expectedWords.filter(w => combined.includes(w)); - if (!hasText) { + if (found.length === 0) { throw new Error( - `Transcripts arrived but contained no text.\n` + + `Transcripts arrived but no expected words found.\n` + `Got: ${transcripts.slice(0, 3).join(' | ')}`, ); } - console.log(`✓ Transcript content verified (received text from Deepgram)`); + console.log(`✓ Transcript content verified (found: ${found.join(', ')})`); } finally { server.close(); From f8a6a408d47708ef4f0f8123afe86f77b360128c Mon Sep 17 00:00:00 2001 From: examples-bot Date: Mon, 30 Mar 2026 13:26:55 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(examples):=20revert=20boolean=E2=86=92s?= =?UTF-8?q?tring=20regression=20for=20SDK=20options=20in=20020-twilio-medi?= =?UTF-8?q?a-streams-node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/020-twilio-media-streams-node/src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/020-twilio-media-streams-node/src/index.js b/examples/020-twilio-media-streams-node/src/index.js index 7ab0d0f..1c63f0f 100644 --- a/examples/020-twilio-media-streams-node/src/index.js +++ b/examples/020-twilio-media-streams-node/src/index.js @@ -14,8 +14,8 @@ const DEEPGRAM_LIVE_OPTIONS = { encoding: 'mulaw', sample_rate: 8000, channels: 1, - smart_format: 'true', - interim_results: 'true', + smart_format: true, + interim_results: true, utterance_end_ms: 1000, };