Files
EveFlow/tests/keywords.test.ts
T
Claude 0d284fea9e feat: always-on wake word with sherpa-onnx keyword spotting (v2.2.0)
- Catalog: 3.3 MB zipformer keyword-spotting model (kws-en).
- shared/keywords: BPE encoding of wake phrases (SentencePiece table for common words,
  greedy longest-match fallback over the model vocabulary) and keywords file builder.
- Worker: KeywordSpotter stream fed with 16-bit PCM, detections pushed as unsolicited
  messages; engine derives the keywords file, maps sensitivity to threshold/score,
  forwards detections to the renderer and re-arms after a worker restart.
- Renderer: WakeListener keeps one microphone stream, batches 256 ms frames to the
  spotter and captures the command on the same stream after detection (pre-roll, VAD),
  then resumes spotting; the wake word also interrupts speech. Settings: wake mode
  (off / always-on / transcript filter), keyword, sensitivity, status and one-click
  model download; HUD caption shows the active keyword.
- Validated: detection in the worker (fork) and through the real Electron IPC path on
  Kokoro audio, no false positive on an English recording; 23 unit tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y
2026-09-03 18:21:35 +00:00

26 lines
1.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildKeywordsFile, encodeKeyword, normalizeKeyword, parseTokens } from '../shared/keywords';
const vocab = parseTokens(['<blk> 0', '▁ 3', '▁JA 4', 'R 5', 'VI 6', 'S 7', '▁HE 8', 'Y 9', '▁NO 10', 'V 11', 'A 12', '▁MA 13', 'X 14', '▁T 15', 'ON 16', 'Y 17'].join('\n'));
describe('keywords', () => {
it('normalises accents, case and punctuation', () => {
expect(normalizeKeyword(' Hé, Jarvis ! ')).toBe('HE JARVIS');
});
it('uses the known SentencePiece encodings', () => {
expect(encodeKeyword('jarvis', vocab)).toBe('▁JA R VI S');
expect(encodeKeyword('Hey Jarvis', vocab)).toBe('▁HE Y ▁JA R VI S');
});
it('falls back to greedy longest match', () => {
expect(encodeKeyword('max', vocab)).toBe('▁MA X');
expect(encodeKeyword('tony', vocab)).toBe('▁T ON Y');
expect(encodeKeyword('zzz', vocab)).toBeNull();
});
it('builds a keywords file with labels', () => {
const file = buildKeywordsFile(['jarvis', 'hey jarvis', 'zzz'], vocab);
expect(file.content).toBe('▁JA R VI S @jarvis\n▁HE Y ▁JA R VI S @hey_jarvis\n');
expect(file.accepted).toEqual(['jarvis', 'hey_jarvis']);
expect(file.rejected).toEqual(['zzz']);
});
});