+76
-10
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user