ti-pote/apps/robot-client/scripts/test_wakeword.ts
2026-04-09 13:19:51 +02:00

151 lines
4.5 KiB
TypeScript

#!/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 <file> 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);