File size: 5,503 Bytes
b8cc2bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
 * Integration tests for buffer.worker.ts (v4 centralized multi-layer data store).
 *
 * Ensures the BufferWorker:
 *   - Loads and responds to INIT with the v4 layer config
 *   - Accepts WRITE to VAD layers and responds to HAS_SPEECH / GET_SILENCE_TAIL
 *   - Resets state on RESET
 *
 * Run: npm test
 */

import '@vitest/web-worker';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { BufferWorkerConfig } from './types';

function defaultConfig(): BufferWorkerConfig {
    return {
        sampleRate: 16000,
        layers: {
            audio: { hopSamples: 1, entryDimension: 1, maxDurationSec: 30 },
            mel: { hopSamples: 160, entryDimension: 128, maxDurationSec: 30 },
            energyVad: { hopSamples: 1280, entryDimension: 1, maxDurationSec: 30 },
            inferenceVad: { hopSamples: 256, entryDimension: 1, maxDurationSec: 30 },
        },
    };
}

function sendRequest(
    worker: Worker,
    type: string,
    payload: any,
    id: number
): Promise<{ type: string; id?: number; payload?: any }> {
    return new Promise((resolve, reject) => {
        const timeout = setTimeout(() => reject(new Error(`Worker ${type} timed out`)), 5000);
        const handler = (e: MessageEvent) => {
            const data = e.data as { type: string; id?: number; payload?: any };
            if (data.type === 'ERROR' && data.id === id) {
                clearTimeout(timeout);
                worker.removeEventListener('message', handler);
                reject(new Error(data.payload));
                return;
            }
            if (data.id === id || (type === 'INIT' && data.type === 'INIT')) {
                clearTimeout(timeout);
                worker.removeEventListener('message', handler);
                resolve(data);
                return;
            }
        };
        worker.addEventListener('message', handler);
        worker.postMessage({ type, payload, id });
    });
}

describe('buffer.worker', () => {
    let worker: Worker;
    let nextId: number;

    beforeEach(() => {
        worker = new Worker(new URL('./buffer.worker.ts', import.meta.url), {
            type: 'module',
        });
        nextId = 1;
    });

    afterEach(() => {
        worker.terminate();
    });

    it('should load without errors', async () => {
        const errPromise = new Promise<ErrorEvent>((resolve) => {
            worker.onerror = (e) => resolve(e as ErrorEvent);
        });
        const initPromise = sendRequest(worker, 'INIT', defaultConfig(), nextId++);
        const result = await Promise.race([
            initPromise.then(() => 'ok'),
            errPromise.then((e) => {
                throw new Error(`Worker load failed: ${e.message}`);
            }),
        ]);
        expect(result).toBe('ok');
    });

    it('should respond to INIT with success', async () => {
        const response = await sendRequest(worker, 'INIT', defaultConfig(), nextId++);
        expect(response.type).toBe('INIT');
        expect(response.payload?.success).toBe(true);
    });

    it('should return no speech and zero silence tail before any write', async () => {
        await sendRequest(worker, 'INIT', defaultConfig(), nextId++);

        const hasSpeech = await sendRequest(
            worker,
            'HAS_SPEECH',
            { layer: 'energyVad', startSample: 0, endSample: 1280, threshold: 0.3 },
            nextId++
        );
        expect(hasSpeech.type).toBe('HAS_SPEECH');
        expect(hasSpeech.payload?.hasSpeech).toBe(false);

        const silence = await sendRequest(
            worker,
            'GET_SILENCE_TAIL',
            { layer: 'energyVad', threshold: 0.3 },
            nextId++
        );
        expect(silence.type).toBe('GET_SILENCE_TAIL');
        expect(silence.payload?.durationSec).toBe(0);
    });

    it('should report speech after writing above-threshold values to energyVad', async () => {
        await sendRequest(worker, 'INIT', defaultConfig(), nextId++);

        worker.postMessage({
            type: 'WRITE',
            payload: { layer: 'energyVad', data: [0.9] },
        });
        await new Promise((r) => setTimeout(r, 50));

        const hasSpeech = await sendRequest(
            worker,
            'HAS_SPEECH',
            { layer: 'energyVad', startSample: 0, endSample: 1280, threshold: 0.3 },
            nextId++
        );
        expect(hasSpeech.payload?.hasSpeech).toBe(true);
        expect(hasSpeech.payload?.maxProb).toBeGreaterThanOrEqual(0.3);
    });

    it('should return silence tail duration after writing silence', async () => {
        await sendRequest(worker, 'INIT', defaultConfig(), nextId++);

        worker.postMessage({ type: 'WRITE', payload: { layer: 'energyVad', data: [0.1] } });
        worker.postMessage({ type: 'WRITE', payload: { layer: 'energyVad', data: [0.1] } });
        await new Promise((r) => setTimeout(r, 50));

        const silence = await sendRequest(
            worker,
            'GET_SILENCE_TAIL',
            { layer: 'energyVad', threshold: 0.3 },
            nextId++
        );
        expect(silence.payload?.durationSec).toBeGreaterThan(0);
    });

    it('should respond to RESET with success', async () => {
        await sendRequest(worker, 'INIT', defaultConfig(), nextId++);
        const response = await sendRequest(worker, 'RESET', undefined, nextId++);
        expect(response.type).toBe('RESET');
        expect(response.payload?.success).toBe(true);
    });
});