Updates
Rolling release / release (push) Successful in 7m21s

This commit is contained in:
2026-09-12 09:40:42 -04:00
parent a4073b9020
commit f26204505e
23 changed files with 531 additions and 114 deletions
+76 -10
View File
@@ -117,7 +117,7 @@ test('PCM packing uses even s16le sample pairs', () => {
assert.equal(fromBytes.samples[0], 256);
});
test('ASR failure preserves speech output and reports unavailable microphone', async () => {
test('ASR failure keeps the microphone hot and still allows speech output', async () => {
const capture = new EventEmitter(); let captured = false;
capture.start = () => { captured = true; }; capture.stop = () => {};
let spoken = '';
@@ -127,12 +127,14 @@ test('ASR failure preserves speech output and reports unavailable microphone', a
playback: { play: async () => {}, stop() {} },
});
loop.on('error', () => {});
await loop.start(); await loop.speak('Speech still works.');
assert.equal(spoken, 'Speech still works.'); assert.equal(captured, false);
assert.equal(loop.status.tts, true); assert.equal(loop.status.asr, false);
spoken = '';
await loop.speak('OK.');
assert.equal(spoken, 'OK.');
await loop.start();
assert.equal(captured, true);
assert.equal(loop.status.asr, false);
assert.equal(loop.status.tts, false);
await loop.speak('Speech still works.');
assert.equal(spoken, 'Speech still works.');
assert.equal(loop.status.tts, true);
assert.equal(await loop.ensureAsr(), false);
assert.match(loop.status.errors.asr, /Whisper/); await loop.stop();
});
@@ -142,8 +144,11 @@ test('TTS failure preserves ASR and push to talk without wake command', async ()
asr: { start: async () => {} }, tts: { start: async () => { throw new Error('TTS failed'); } },
playback: { stop() {} },
});
loop.on('error', () => {}); await loop.start(); loop.setPushToTalk(true);
loop.on('error', () => {}); await loop.start();
assert.equal(loop.status.asr, false);
await loop.setPushToTalk(true);
assert.equal(loop.status.asr, true); assert.equal(loop.ptt, true);
assert.equal(await loop.ensureTts(), false);
assert.equal(loop.status.tts, false); assert.equal(loop.status.wake, false); await loop.stop();
});
@@ -169,7 +174,7 @@ test('voice loop retries microphone capture after PipeWire comes up', async () =
});
loop.on('error', () => {});
await loop.start();
assert.equal(loop.status.asr, true);
assert.equal(loop.status.asr, false);
await new Promise((resolve) => setTimeout(resolve, 5));
assert.equal(loop.status.capture, false);
await new Promise((resolve) => setTimeout(resolve, 40));
@@ -194,7 +199,10 @@ test('voice loop retries ASR after a first-login GPU miss', async () => {
loop.on('error', () => {});
await loop.start();
assert.equal(loop.status.asr, false);
assert.equal(capture.started, undefined);
assert.equal(capture.started, true);
assert.equal(attempts, 0);
assert.equal(await loop.ensureAsr(), false);
assert.equal(attempts, 1);
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(attempts, 2);
assert.equal(loop.status.asr, true);
@@ -207,3 +215,61 @@ test('voice adapters load canonical ASR and TTS plugins', () => {
assert.match(source, /whispercpp-transcription/);
assert.match(source, /tts-ggml/);
});
function tonePcm(ms, amplitude = 12_000, hz = 220) {
const samples = Math.floor(16_000 * ms / 1000);
const buf = Buffer.alloc(samples * 2);
for (let i = 0; i < samples; i++) buf.writeInt16LE(Math.floor(amplitude * Math.sin(2 * Math.PI * hz * i / 16_000)), i * 2);
return buf;
}
test('built-in CPU wake detector fires on a two-peak hey-jarvis cadence', async () => {
const { KeywordWakeEngine, createWakeEngine } = await import('../daemon/wake-engine.js');
const engine = createWakeEngine({ command: 'jarvis-wake-bridge', phrases: ['hey jarvis'] });
assert.equal(engine instanceof KeywordWakeEngine, true);
const heard = [];
engine.on('wake', (phrase) => heard.push(phrase));
engine.push(Buffer.concat([tonePcm(80, 0), tonePcm(220), tonePcm(90, 0), tonePcm(280), tonePcm(200, 0)]));
assert.deepEqual(heard, ['hey jarvis']);
engine.push(tonePcm(800));
assert.equal(heard.length, 1);
});
test('sleeping voice loop still feeds the wake detector and skips VAD', () => {
const capture = new EventEmitter(); capture.start = () => {}; capture.stop = () => {};
let wakeFrames = 0; let vadFrames = 0;
const wake = new WakeEngine({ detect: () => null });
wake.push = () => { wakeFrames += 1; };
const vad = new VadSegmenter();
vad.push = () => { vadFrames += 1; };
const daemon = new EventEmitter(); daemon.state = 'SLEEPING';
const loop = new VoiceLoop({ daemon, capture, wake, vad });
loop.running = true;
loop.pushAudio(Buffer.alloc(640));
assert.equal(wakeFrames, 1);
assert.equal(vadFrames, 0);
});
test('voice start keeps the microphone hot without loading ASR or TTS', async () => {
const capture = new EventEmitter(); let captured = false;
capture.start = () => { captured = true; }; capture.stop = () => {};
let asrStarts = 0; let ttsStarts = 0;
const loop = new VoiceLoop({
capture,
asr: { start: async () => { asrStarts += 1; } },
tts: { start: async () => { ttsStarts += 1; } },
wake: new WakeEngine({ detect: () => null }),
});
await loop.start();
assert.equal(captured, true);
assert.equal(loop.status.capture, true);
assert.equal(loop.status.asr, false);
assert.equal(loop.status.tts, false);
assert.equal(asrStarts, 0);
assert.equal(ttsStarts, 0);
assert.equal(loop.status.wake, true);
await loop.parkModels();
assert.equal(loop.status.asr, false);
assert.equal(loop.status.tts, false);
await loop.stop();
});