#!/usr/bin/env npx tsx /** * Test wake word detection using live ESP32 audio. * * Usage: * npx tsx scripts/test_wakeword.ts [--threshold 0.5] [--record out.raw] * * Connects to the ESP32 via serial, reads AUDIO_UP frames, and pipes * the raw PCM into the wake_word.py subprocess. Prints detections live. * * --record Also dump raw PCM to a file so you can replay it later: * python3 scripts/wake_word.py --model hey_jarvis --input stdin < out.raw */ import { SerialPort } from 'serialport'; import { spawn, type ChildProcess } from 'node:child_process'; import { createWriteStream, type WriteStream } from 'node:fs'; import { parseArgs } from 'node:util'; import { FrameDecoder, MsgType, encodeFrame } from '../src/hardware/protocol.js'; const { values } = parseArgs({ options: { threshold: { type: 'string', default: '0.5' }, record: { type: 'string' }, model: { type: 'string', default: 'hey_jarvis' }, python: { type: 'string', default: process.env.WAKEWORD_PYTHON_PATH || 'python3' }, port: { type: 'string', default: '/dev/serial0' }, baud: { type: 'string', default: '921600' }, }, }); const threshold = values.threshold!; const model = values.model!; const pythonPath = values.python!; const serialPath = values.port!; const baudRate = parseInt(values.baud!, 10); let recordStream: WriteStream | null = null; if (values.record) { recordStream = createWriteStream(values.record); console.log(`šŸ“ Recording raw PCM to ${values.record}`); } // ── Spawn Python wake word process ── const pyArgs = [ './scripts/wake_word.py', '--model', model, '--threshold', threshold, '--sample-rate', '16000', '--input', 'stdin', '--control-fd', '3', ]; console.log(`šŸ Spawning: ${pythonPath} ${pyArgs.join(' ')}`); console.log(`šŸŽ¤ Threshold: ${threshold} | Model: ${model}`); console.log(`šŸ”Œ Serial: ${serialPath} @ ${baudRate}\n`); const py: ChildProcess = spawn(pythonPath, pyArgs, { stdio: ['pipe', 'pipe', 'pipe', 'pipe'], }); py.stdout?.on('data', (data: Buffer) => { const lines = data.toString().trim().split('\n'); for (const line of lines) { if (line.trim() === 'DETECTED') { console.log(`\n🟢 DETECTED at ${new Date().toLocaleTimeString()}\n`); } } }); py.stderr?.on('data', (data: Buffer) => { const lines = data.toString().trim().split('\n'); for (const line of lines) { const msg = line.trim(); if (msg === 'READY') { console.log('āœ… Wake word engine ready — say "Hey Jarvis"!\n'); } else if (msg.startsWith('Loading')) { console.log(`ā³ ${msg}`); } else if (msg.startsWith('Wake word model loaded')) { console.log(`āœ… ${msg}`); } else if (!msg.includes('onnxruntime') && !msg.includes('UserWarning') && !msg.includes('warnings.warn')) { console.log(` [py] ${msg}`); } } }); py.on('exit', (code) => { console.log(`\nāŒ Python process exited with code ${code}`); process.exit(code ?? 1); }); // ── Open serial and forward AUDIO_UP to Python stdin ── let audioChunks = 0; const decoder = new FrameDecoder((frame) => { if (frame.type === MsgType.AUDIO_UP) { audioChunks++; if (py.stdin && !py.stdin.destroyed) { py.stdin.write(frame.payload); } if (recordStream) { recordStream.write(frame.payload); } // Progress indicator every ~1s (assuming ~100ms chunks) if (audioChunks % 10 === 0) { process.stdout.write(`\ršŸŽ§ Audio chunks: ${audioChunks} `); } } }); const serial = new SerialPort({ path: serialPath, baudRate, autoOpen: false }); serial.on('data', (chunk: Buffer) => decoder.feed(chunk)); serial.on('error', (err) => { console.error('Serial error:', err.message); process.exit(1); }); serial.open((err) => { if (err) { console.error(`Failed to open ${serialPath}:`, err.message); process.exit(1); } console.log(`šŸ”Œ Serial port open: ${serialPath}`); // Send heartbeat so ESP32 stays active setInterval(() => { if (serial.isOpen) serial.write(encodeFrame(MsgType.STATUS)); }, 1000); }); // ── Graceful shutdown ── function cleanup() { console.log('\n\nShutting down...'); if (recordStream) { recordStream.end(); console.log(`šŸ“ Recording saved`); } const control = py.stdio[3] as unknown as NodeJS.WritableStream | null; if (control && !(control as any).destroyed) { control.write('QUIT\n'); } setTimeout(() => { py.kill('SIGTERM'); serial.close(); process.exit(0); }, 500); } process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup);