@volley/recognition-client-sdk 0.1.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -0
- package/dist/browser-CDQ_TzeH.d.ts +1039 -0
- package/dist/index.d.ts +461 -0
- package/dist/index.js +2332 -0
- package/dist/index.js.map +1 -0
- package/dist/recog-client-sdk.browser.d.ts +2 -0
- package/dist/recog-client-sdk.browser.js +1843 -0
- package/dist/recog-client-sdk.browser.js.map +1 -0
- package/package.json +73 -0
- package/src/browser.ts +24 -0
- package/src/config-builder.ts +213 -0
- package/src/factory.ts +43 -0
- package/src/index.ts +86 -0
- package/src/recognition-client.spec.ts +551 -0
- package/src/recognition-client.ts +595 -0
- package/src/recognition-client.types.ts +260 -0
- package/src/simplified-vgf-recognition-client.spec.ts +671 -0
- package/src/simplified-vgf-recognition-client.ts +339 -0
- package/src/utils/audio-ring-buffer.ts +170 -0
- package/src/utils/message-handler.ts +131 -0
- package/src/utils/url-builder.ts +70 -0
- package/src/vgf-recognition-mapper.ts +225 -0
- package/src/vgf-recognition-state.ts +89 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for SimplifiedVGFRecognitionClient
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SimplifiedVGFRecognitionClient, createSimplifiedVGFClient } from './simplified-vgf-recognition-client.js';
|
|
6
|
+
import { RealTimeTwoWayWebSocketRecognitionClient } from './recognition-client.js';
|
|
7
|
+
import { ClientState } from './recognition-client.types.js';
|
|
8
|
+
import { AudioEncoding, RecognitionContextTypeV1 } from '@recog/shared-types';
|
|
9
|
+
import {
|
|
10
|
+
RecordingStatus,
|
|
11
|
+
TranscriptionStatus,
|
|
12
|
+
type RecognitionState
|
|
13
|
+
} from './vgf-recognition-state.js';
|
|
14
|
+
|
|
15
|
+
// Mock the underlying client
|
|
16
|
+
jest.mock('./recognition-client');
|
|
17
|
+
|
|
18
|
+
describe('SimplifiedVGFRecognitionClient', () => {
|
|
19
|
+
let mockClient: jest.Mocked<RealTimeTwoWayWebSocketRecognitionClient>;
|
|
20
|
+
let simplifiedClient: SimplifiedVGFRecognitionClient;
|
|
21
|
+
let stateChangeCallback: jest.Mock;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
// Reset mocks
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
|
|
27
|
+
// Create mock for underlying client
|
|
28
|
+
mockClient = {
|
|
29
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
30
|
+
sendAudio: jest.fn(),
|
|
31
|
+
stopRecording: jest.fn().mockResolvedValue(undefined),
|
|
32
|
+
getAudioUtteranceId: jest.fn().mockReturnValue('test-uuid'),
|
|
33
|
+
getState: jest.fn().mockReturnValue(ClientState.INITIAL),
|
|
34
|
+
isConnected: jest.fn().mockReturnValue(false),
|
|
35
|
+
isConnecting: jest.fn().mockReturnValue(false),
|
|
36
|
+
isStopping: jest.fn().mockReturnValue(false),
|
|
37
|
+
isTranscriptionFinished: jest.fn().mockReturnValue(false),
|
|
38
|
+
isBufferOverflowing: jest.fn().mockReturnValue(false),
|
|
39
|
+
} as any;
|
|
40
|
+
|
|
41
|
+
// Mock the constructor
|
|
42
|
+
(RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>)
|
|
43
|
+
.mockImplementation(() => mockClient as any);
|
|
44
|
+
|
|
45
|
+
stateChangeCallback = jest.fn();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Constructor', () => {
|
|
49
|
+
it('should initialize with correct default VGF state', () => {
|
|
50
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
51
|
+
asrRequestConfig: {
|
|
52
|
+
provider: 'deepgram',
|
|
53
|
+
language: 'en',
|
|
54
|
+
sampleRate: 16000,
|
|
55
|
+
encoding: AudioEncoding.LINEAR16
|
|
56
|
+
},
|
|
57
|
+
onStateChange: stateChangeCallback
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const state = simplifiedClient.getVGFState();
|
|
61
|
+
expect(state.audioUtteranceId).toBeDefined();
|
|
62
|
+
expect(state.startRecordingStatus).toBe(RecordingStatus.READY);
|
|
63
|
+
expect(state.transcriptionStatus).toBe(TranscriptionStatus.NOT_STARTED);
|
|
64
|
+
expect(state.pendingTranscript).toBe('');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should accept initial state and use its audioUtteranceId', () => {
|
|
68
|
+
const initialState: RecognitionState = {
|
|
69
|
+
audioUtteranceId: 'existing-session-id',
|
|
70
|
+
startRecordingStatus: RecordingStatus.FINISHED,
|
|
71
|
+
finalTranscript: 'Previous transcript',
|
|
72
|
+
pendingTranscript: '', // Required field
|
|
73
|
+
transcriptionStatus: TranscriptionStatus.FINALIZED
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
77
|
+
initialState,
|
|
78
|
+
onStateChange: stateChangeCallback
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const state = simplifiedClient.getVGFState();
|
|
82
|
+
expect(state.audioUtteranceId).toBe('existing-session-id');
|
|
83
|
+
expect(state.finalTranscript).toBe('Previous transcript');
|
|
84
|
+
|
|
85
|
+
// Verify audioUtteranceId was passed to underlying client
|
|
86
|
+
const constructorCalls = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls;
|
|
87
|
+
expect(constructorCalls[0]?.[0]?.audioUtteranceId).toBe('existing-session-id');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should store ASR config as JSON string', () => {
|
|
91
|
+
const asrConfig = {
|
|
92
|
+
provider: 'deepgram' as const,
|
|
93
|
+
language: 'en',
|
|
94
|
+
model: 'nova-2',
|
|
95
|
+
sampleRate: 16000,
|
|
96
|
+
encoding: AudioEncoding.LINEAR16
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
100
|
+
asrRequestConfig: asrConfig,
|
|
101
|
+
onStateChange: stateChangeCallback
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const state = simplifiedClient.getVGFState();
|
|
105
|
+
expect(state.asrConfig).toBe(JSON.stringify(asrConfig));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('State Management', () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
112
|
+
asrRequestConfig: {
|
|
113
|
+
provider: 'deepgram',
|
|
114
|
+
language: 'en',
|
|
115
|
+
sampleRate: 16000,
|
|
116
|
+
encoding: AudioEncoding.LINEAR16
|
|
117
|
+
},
|
|
118
|
+
onStateChange: stateChangeCallback
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should update state to RECORDING when sendAudio is called', () => {
|
|
123
|
+
const audioData = Buffer.from([1, 2, 3, 4]);
|
|
124
|
+
simplifiedClient.sendAudio(audioData);
|
|
125
|
+
|
|
126
|
+
expect(stateChangeCallback).toHaveBeenCalled();
|
|
127
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
128
|
+
expect(updatedState.startRecordingStatus).toBe(RecordingStatus.RECORDING);
|
|
129
|
+
expect(updatedState.startRecordingTimestamp).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should only set recording timestamp once', () => {
|
|
133
|
+
const audioData = Buffer.from([1, 2, 3, 4]);
|
|
134
|
+
|
|
135
|
+
simplifiedClient.sendAudio(audioData);
|
|
136
|
+
const firstTimestamp = stateChangeCallback.mock.calls[0][0].startRecordingTimestamp;
|
|
137
|
+
|
|
138
|
+
// Clear mock to verify no additional state changes
|
|
139
|
+
stateChangeCallback.mockClear();
|
|
140
|
+
|
|
141
|
+
// Second sendAudio should not trigger state change since already recording
|
|
142
|
+
simplifiedClient.sendAudio(audioData);
|
|
143
|
+
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
144
|
+
|
|
145
|
+
// Verify timestamp hasn't changed in internal state
|
|
146
|
+
const currentState = simplifiedClient.getVGFState();
|
|
147
|
+
expect(currentState.startRecordingTimestamp).toBe(firstTimestamp);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should update state to FINISHED when stopRecording is called', async () => {
|
|
151
|
+
await simplifiedClient.stopRecording();
|
|
152
|
+
|
|
153
|
+
expect(stateChangeCallback).toHaveBeenCalled();
|
|
154
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
155
|
+
expect(updatedState.startRecordingStatus).toBe(RecordingStatus.FINISHED);
|
|
156
|
+
expect(updatedState.finalRecordingTimestamp).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Transcript Callbacks', () => {
|
|
161
|
+
let onTranscriptCallback: (result: any) => void;
|
|
162
|
+
let onMetadataCallback: (metadata: any) => void;
|
|
163
|
+
let onErrorCallback: (error: any) => void;
|
|
164
|
+
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
// Capture the callbacks passed to underlying client
|
|
167
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
168
|
+
asrRequestConfig: {
|
|
169
|
+
provider: 'deepgram',
|
|
170
|
+
language: 'en',
|
|
171
|
+
sampleRate: 16000,
|
|
172
|
+
encoding: AudioEncoding.LINEAR16
|
|
173
|
+
},
|
|
174
|
+
onStateChange: stateChangeCallback
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
178
|
+
onTranscriptCallback = constructorCall?.onTranscript ?? jest.fn();
|
|
179
|
+
onMetadataCallback = constructorCall?.onMetadata ?? jest.fn();
|
|
180
|
+
onErrorCallback = constructorCall?.onError ?? jest.fn();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should directly copy pending transcript without combining', () => {
|
|
184
|
+
const transcriptResult = {
|
|
185
|
+
finalTranscript: 'Hello',
|
|
186
|
+
pendingTranscript: ' world',
|
|
187
|
+
pendingTranscriptConfidence: 0.85,
|
|
188
|
+
finalTranscriptConfidence: 0.95,
|
|
189
|
+
is_finished: false
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
onTranscriptCallback(transcriptResult);
|
|
193
|
+
|
|
194
|
+
expect(stateChangeCallback).toHaveBeenCalled();
|
|
195
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
196
|
+
expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.IN_PROGRESS);
|
|
197
|
+
// Should be direct copy, NOT combined
|
|
198
|
+
expect(updatedState.pendingTranscript).toBe(' world');
|
|
199
|
+
expect(updatedState.pendingConfidence).toBe(0.85);
|
|
200
|
+
// Final should also be copied when present
|
|
201
|
+
expect(updatedState.finalTranscript).toBe('Hello');
|
|
202
|
+
expect(updatedState.finalConfidence).toBe(0.95);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should update VGF state with final transcript', () => {
|
|
206
|
+
const transcriptResult = {
|
|
207
|
+
finalTranscript: 'Hello world',
|
|
208
|
+
finalTranscriptConfidence: 0.98,
|
|
209
|
+
is_finished: true
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
onTranscriptCallback(transcriptResult);
|
|
213
|
+
|
|
214
|
+
expect(stateChangeCallback).toHaveBeenCalled();
|
|
215
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
216
|
+
expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.FINALIZED);
|
|
217
|
+
expect(updatedState.finalTranscript).toBe('Hello world');
|
|
218
|
+
expect(updatedState.finalConfidence).toBe(0.98);
|
|
219
|
+
expect(updatedState.pendingTranscript).toBe('');
|
|
220
|
+
expect(updatedState.finalTranscriptionTimestamp).toBeDefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should handle metadata and mark recording as finished', () => {
|
|
224
|
+
const metadata = {
|
|
225
|
+
audioUtteranceId: 'test-uuid',
|
|
226
|
+
duration: 5000
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
onMetadataCallback(metadata);
|
|
230
|
+
|
|
231
|
+
expect(stateChangeCallback).toHaveBeenCalled();
|
|
232
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
233
|
+
expect(updatedState.startRecordingStatus).toBe(RecordingStatus.FINISHED);
|
|
234
|
+
expect(updatedState.finalRecordingTimestamp).toBeDefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle errors and update state', () => {
|
|
238
|
+
const error = {
|
|
239
|
+
message: 'Recognition failed',
|
|
240
|
+
code: 'RECOGNITION_ERROR'
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
onErrorCallback(error);
|
|
244
|
+
|
|
245
|
+
expect(stateChangeCallback).toHaveBeenCalled();
|
|
246
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
247
|
+
expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.ERROR);
|
|
248
|
+
expect(updatedState.startRecordingStatus).toBe(RecordingStatus.FINISHED);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should reset isRecordingAudio on error', () => {
|
|
252
|
+
// First start recording
|
|
253
|
+
simplifiedClient.sendAudio(Buffer.from([1, 2, 3]));
|
|
254
|
+
|
|
255
|
+
// Then error occurs
|
|
256
|
+
onErrorCallback({ message: 'Error' });
|
|
257
|
+
|
|
258
|
+
// Send audio again should restart recording
|
|
259
|
+
stateChangeCallback.mockClear();
|
|
260
|
+
simplifiedClient.sendAudio(Buffer.from([4, 5, 6]));
|
|
261
|
+
|
|
262
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
263
|
+
expect(updatedState.startRecordingStatus).toBe(RecordingStatus.RECORDING);
|
|
264
|
+
expect(updatedState.startRecordingTimestamp).toBeDefined();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('Method Delegation', () => {
|
|
269
|
+
beforeEach(() => {
|
|
270
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
271
|
+
asrRequestConfig: {
|
|
272
|
+
provider: 'deepgram',
|
|
273
|
+
language: 'en',
|
|
274
|
+
sampleRate: 16000,
|
|
275
|
+
encoding: AudioEncoding.LINEAR16
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should delegate connect() to underlying client', async () => {
|
|
281
|
+
await simplifiedClient.connect();
|
|
282
|
+
expect(mockClient.connect).toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should delegate sendAudio() to underlying client', () => {
|
|
286
|
+
const audioData = Buffer.from([1, 2, 3, 4]);
|
|
287
|
+
simplifiedClient.sendAudio(audioData);
|
|
288
|
+
expect(mockClient.sendAudio).toHaveBeenCalledWith(audioData);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should delegate stopRecording() to underlying client', async () => {
|
|
292
|
+
await simplifiedClient.stopRecording();
|
|
293
|
+
expect(mockClient.stopRecording).toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should delegate status check methods', () => {
|
|
297
|
+
simplifiedClient.isConnected();
|
|
298
|
+
expect(mockClient.isConnected).toHaveBeenCalled();
|
|
299
|
+
|
|
300
|
+
simplifiedClient.isConnecting();
|
|
301
|
+
expect(mockClient.isConnecting).toHaveBeenCalled();
|
|
302
|
+
|
|
303
|
+
simplifiedClient.isStopping();
|
|
304
|
+
expect(mockClient.isStopping).toHaveBeenCalled();
|
|
305
|
+
|
|
306
|
+
simplifiedClient.isTranscriptionFinished();
|
|
307
|
+
expect(mockClient.isTranscriptionFinished).toHaveBeenCalled();
|
|
308
|
+
|
|
309
|
+
simplifiedClient.isBufferOverflowing();
|
|
310
|
+
expect(mockClient.isBufferOverflowing).toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should delegate getAudioUtteranceId()', () => {
|
|
314
|
+
const id = simplifiedClient.getAudioUtteranceId();
|
|
315
|
+
expect(mockClient.getAudioUtteranceId).toHaveBeenCalled();
|
|
316
|
+
expect(id).toBe('test-uuid');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should delegate getState()', () => {
|
|
320
|
+
const state = simplifiedClient.getState();
|
|
321
|
+
expect(mockClient.getState).toHaveBeenCalled();
|
|
322
|
+
expect(state).toBe(ClientState.INITIAL);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Original Callbacks', () => {
|
|
327
|
+
it('should call original onTranscript callback if provided', () => {
|
|
328
|
+
const originalOnTranscript = jest.fn();
|
|
329
|
+
|
|
330
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
331
|
+
asrRequestConfig: {
|
|
332
|
+
provider: 'deepgram',
|
|
333
|
+
language: 'en',
|
|
334
|
+
sampleRate: 16000,
|
|
335
|
+
encoding: AudioEncoding.LINEAR16
|
|
336
|
+
},
|
|
337
|
+
onTranscript: originalOnTranscript
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
341
|
+
const wrappedCallback = constructorCall?.onTranscript;
|
|
342
|
+
if (!wrappedCallback) throw new Error('onTranscript callback not found');
|
|
343
|
+
|
|
344
|
+
const result = { finalTranscript: 'test', is_finished: false };
|
|
345
|
+
wrappedCallback(result as any);
|
|
346
|
+
|
|
347
|
+
expect(originalOnTranscript).toHaveBeenCalledWith(result);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should call original onError callback if provided', () => {
|
|
351
|
+
const originalOnError = jest.fn();
|
|
352
|
+
|
|
353
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
354
|
+
asrRequestConfig: {
|
|
355
|
+
provider: 'deepgram',
|
|
356
|
+
language: 'en',
|
|
357
|
+
sampleRate: 16000,
|
|
358
|
+
encoding: AudioEncoding.LINEAR16
|
|
359
|
+
},
|
|
360
|
+
onError: originalOnError
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
364
|
+
const wrappedCallback = constructorCall?.onError;
|
|
365
|
+
if (!wrappedCallback) throw new Error('onError callback not found');
|
|
366
|
+
|
|
367
|
+
const error = { message: 'test error' };
|
|
368
|
+
wrappedCallback(error as any);
|
|
369
|
+
|
|
370
|
+
expect(originalOnError).toHaveBeenCalledWith(error);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('Thin Layer Verification', () => {
|
|
375
|
+
it('should pass transcript result directly to mapper without modification', () => {
|
|
376
|
+
const transcriptResult = {
|
|
377
|
+
type: 'Transcription',
|
|
378
|
+
audioUtteranceId: 'test-123',
|
|
379
|
+
finalTranscript: 'Final text',
|
|
380
|
+
pendingTranscript: 'Pending text',
|
|
381
|
+
finalTranscriptConfidence: 0.99,
|
|
382
|
+
pendingTranscriptConfidence: 0.88,
|
|
383
|
+
is_finished: false,
|
|
384
|
+
voiceStart: 100,
|
|
385
|
+
voiceDuration: 500,
|
|
386
|
+
voiceEnd: 600,
|
|
387
|
+
extraField: 'should be ignored by VGF state'
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
391
|
+
asrRequestConfig: {
|
|
392
|
+
provider: 'deepgram',
|
|
393
|
+
language: 'en',
|
|
394
|
+
sampleRate: 16000,
|
|
395
|
+
encoding: AudioEncoding.LINEAR16
|
|
396
|
+
},
|
|
397
|
+
onStateChange: stateChangeCallback
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
401
|
+
const wrappedCallback = constructorCall?.onTranscript;
|
|
402
|
+
if (!wrappedCallback) throw new Error('onTranscript callback not found');
|
|
403
|
+
|
|
404
|
+
wrappedCallback(transcriptResult as any);
|
|
405
|
+
|
|
406
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
407
|
+
// Verify direct copy without modification
|
|
408
|
+
expect(updatedState.pendingTranscript).toBe('Pending text');
|
|
409
|
+
expect(updatedState.finalTranscript).toBe('Final text');
|
|
410
|
+
expect(updatedState.pendingConfidence).toBe(0.88);
|
|
411
|
+
expect(updatedState.finalConfidence).toBe(0.99);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should handle only pending transcript correctly', () => {
|
|
415
|
+
const transcriptResult = {
|
|
416
|
+
pendingTranscript: 'Just pending',
|
|
417
|
+
pendingTranscriptConfidence: 0.75,
|
|
418
|
+
is_finished: false
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
422
|
+
asrRequestConfig: {
|
|
423
|
+
provider: 'deepgram',
|
|
424
|
+
language: 'en',
|
|
425
|
+
sampleRate: 16000,
|
|
426
|
+
encoding: AudioEncoding.LINEAR16
|
|
427
|
+
},
|
|
428
|
+
onStateChange: stateChangeCallback
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
432
|
+
const wrappedCallback = constructorCall?.onTranscript;
|
|
433
|
+
if (!wrappedCallback) throw new Error('onTranscript callback not found');
|
|
434
|
+
|
|
435
|
+
wrappedCallback(transcriptResult as any);
|
|
436
|
+
|
|
437
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
438
|
+
expect(updatedState.pendingTranscript).toBe('Just pending');
|
|
439
|
+
expect(updatedState.pendingConfidence).toBe(0.75);
|
|
440
|
+
expect(updatedState.finalTranscript).toBeUndefined();
|
|
441
|
+
expect(updatedState.finalConfidence).toBeUndefined();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should handle only final transcript correctly', () => {
|
|
445
|
+
const transcriptResult = {
|
|
446
|
+
finalTranscript: 'Just final',
|
|
447
|
+
finalTranscriptConfidence: 0.92,
|
|
448
|
+
is_finished: false
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
452
|
+
asrRequestConfig: {
|
|
453
|
+
provider: 'deepgram',
|
|
454
|
+
language: 'en',
|
|
455
|
+
sampleRate: 16000,
|
|
456
|
+
encoding: AudioEncoding.LINEAR16
|
|
457
|
+
},
|
|
458
|
+
onStateChange: stateChangeCallback
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
462
|
+
const wrappedCallback = constructorCall?.onTranscript;
|
|
463
|
+
if (!wrappedCallback) throw new Error('onTranscript callback not found');
|
|
464
|
+
|
|
465
|
+
wrappedCallback(transcriptResult as any);
|
|
466
|
+
|
|
467
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
468
|
+
expect(updatedState.pendingTranscript).toBe(''); // Empty string when undefined
|
|
469
|
+
expect(updatedState.pendingConfidence).toBeUndefined();
|
|
470
|
+
expect(updatedState.finalTranscript).toBe('Just final');
|
|
471
|
+
expect(updatedState.finalConfidence).toBe(0.92);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should clear pending when is_finished is true', () => {
|
|
475
|
+
const transcriptResult = {
|
|
476
|
+
finalTranscript: 'Complete transcript',
|
|
477
|
+
pendingTranscript: 'Should be ignored',
|
|
478
|
+
finalTranscriptConfidence: 0.98,
|
|
479
|
+
pendingTranscriptConfidence: 0.77,
|
|
480
|
+
is_finished: true
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
484
|
+
asrRequestConfig: {
|
|
485
|
+
provider: 'deepgram',
|
|
486
|
+
language: 'en',
|
|
487
|
+
sampleRate: 16000,
|
|
488
|
+
encoding: AudioEncoding.LINEAR16
|
|
489
|
+
},
|
|
490
|
+
onStateChange: stateChangeCallback
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
494
|
+
const wrappedCallback = constructorCall?.onTranscript;
|
|
495
|
+
if (!wrappedCallback) throw new Error('onTranscript callback not found');
|
|
496
|
+
|
|
497
|
+
wrappedCallback(transcriptResult as any);
|
|
498
|
+
|
|
499
|
+
const updatedState = stateChangeCallback.mock.calls[0][0];
|
|
500
|
+
expect(updatedState.finalTranscript).toBe('Complete transcript');
|
|
501
|
+
expect(updatedState.finalConfidence).toBe(0.98);
|
|
502
|
+
// Pending should be cleared when finished
|
|
503
|
+
expect(updatedState.pendingTranscript).toBe('');
|
|
504
|
+
expect(updatedState.pendingConfidence).toBeUndefined();
|
|
505
|
+
expect(updatedState.transcriptionStatus).toBe(TranscriptionStatus.FINALIZED);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('Factory Function', () => {
|
|
510
|
+
it('should create SimplifiedVGFRecognitionClient instance', () => {
|
|
511
|
+
const client = createSimplifiedVGFClient({
|
|
512
|
+
asrRequestConfig: {
|
|
513
|
+
provider: 'deepgram',
|
|
514
|
+
language: 'en',
|
|
515
|
+
sampleRate: 16000,
|
|
516
|
+
encoding: AudioEncoding.LINEAR16
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
expect(client).toBeInstanceOf(SimplifiedVGFRecognitionClient);
|
|
521
|
+
expect(client.getVGFState).toBeDefined();
|
|
522
|
+
expect(client.connect).toBeDefined();
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
describe('PromptSlotMap Integration', () => {
|
|
527
|
+
it('should pass promptSlotMap from initial state to gameContext', () => {
|
|
528
|
+
const initialState: RecognitionState = {
|
|
529
|
+
audioUtteranceId: 'test-123',
|
|
530
|
+
pendingTranscript: '', // Required field
|
|
531
|
+
promptSlotMap: {
|
|
532
|
+
'entity1': ['value1', 'value2'],
|
|
533
|
+
'entity2': ['value3']
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const gameContext = {
|
|
538
|
+
type: RecognitionContextTypeV1.GAME_CONTEXT,
|
|
539
|
+
gameId: 'test-game',
|
|
540
|
+
gamePhase: 'test-phase'
|
|
541
|
+
} as const;
|
|
542
|
+
|
|
543
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
544
|
+
asrRequestConfig: {
|
|
545
|
+
provider: 'deepgram',
|
|
546
|
+
language: 'en',
|
|
547
|
+
sampleRate: 16000,
|
|
548
|
+
encoding: AudioEncoding.LINEAR16
|
|
549
|
+
},
|
|
550
|
+
gameContext,
|
|
551
|
+
initialState
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
555
|
+
|
|
556
|
+
// Verify useContext was set to true
|
|
557
|
+
expect(constructorCall?.asrRequestConfig?.useContext).toBe(true);
|
|
558
|
+
|
|
559
|
+
// Verify slotMap was added to gameContext
|
|
560
|
+
expect(constructorCall?.gameContext?.slotMap).toEqual({
|
|
561
|
+
'entity1': ['value1', 'value2'],
|
|
562
|
+
'entity2': ['value3']
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should warn if promptSlotMap exists but no gameContext provided', () => {
|
|
567
|
+
const logger = jest.fn();
|
|
568
|
+
const initialState: RecognitionState = {
|
|
569
|
+
audioUtteranceId: 'test-123',
|
|
570
|
+
pendingTranscript: '', // Required field
|
|
571
|
+
promptSlotMap: {
|
|
572
|
+
'entity1': ['value1']
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
577
|
+
asrRequestConfig: {
|
|
578
|
+
provider: 'deepgram',
|
|
579
|
+
language: 'en',
|
|
580
|
+
sampleRate: 16000,
|
|
581
|
+
encoding: AudioEncoding.LINEAR16
|
|
582
|
+
},
|
|
583
|
+
initialState,
|
|
584
|
+
logger
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
expect(logger).toHaveBeenCalledWith(
|
|
588
|
+
'warn',
|
|
589
|
+
'[VGF] promptSlotMap found but no gameContext provided. SlotMap will not be sent.'
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('should preserve promptSlotMap throughout state changes', () => {
|
|
594
|
+
const initialState: RecognitionState = {
|
|
595
|
+
audioUtteranceId: 'test-123',
|
|
596
|
+
pendingTranscript: '', // Required field
|
|
597
|
+
promptSlotMap: {
|
|
598
|
+
'slots': ['test']
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
603
|
+
asrRequestConfig: {
|
|
604
|
+
provider: 'deepgram',
|
|
605
|
+
language: 'en',
|
|
606
|
+
sampleRate: 16000,
|
|
607
|
+
encoding: AudioEncoding.LINEAR16
|
|
608
|
+
},
|
|
609
|
+
initialState,
|
|
610
|
+
onStateChange: stateChangeCallback
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Send audio and verify promptSlotMap is preserved
|
|
614
|
+
simplifiedClient.sendAudio(Buffer.from([1, 2, 3]));
|
|
615
|
+
|
|
616
|
+
let state = stateChangeCallback.mock.calls[0][0];
|
|
617
|
+
expect(state.promptSlotMap).toEqual({ 'slots': ['test'] });
|
|
618
|
+
|
|
619
|
+
// Simulate transcript and verify preservation
|
|
620
|
+
const constructorCall = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls[0]?.[0];
|
|
621
|
+
const onTranscriptCallback = constructorCall?.onTranscript;
|
|
622
|
+
if (!onTranscriptCallback) throw new Error('onTranscript callback not found');
|
|
623
|
+
|
|
624
|
+
onTranscriptCallback({
|
|
625
|
+
finalTranscript: 'test',
|
|
626
|
+
is_finished: false
|
|
627
|
+
} as any);
|
|
628
|
+
|
|
629
|
+
state = stateChangeCallback.mock.calls[1][0];
|
|
630
|
+
expect(state.promptSlotMap).toEqual({ 'slots': ['test'] });
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe('State Immutability', () => {
|
|
635
|
+
it('should return a copy of VGF state, not a reference', () => {
|
|
636
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
637
|
+
asrRequestConfig: {
|
|
638
|
+
provider: 'deepgram',
|
|
639
|
+
language: 'en',
|
|
640
|
+
sampleRate: 16000,
|
|
641
|
+
encoding: AudioEncoding.LINEAR16
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const state1 = simplifiedClient.getVGFState();
|
|
646
|
+
const state2 = simplifiedClient.getVGFState();
|
|
647
|
+
|
|
648
|
+
expect(state1).not.toBe(state2); // Different object references
|
|
649
|
+
expect(state1).toEqual(state2); // But same content
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should pass a copy of state to onStateChange callback', () => {
|
|
653
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
654
|
+
asrRequestConfig: {
|
|
655
|
+
provider: 'deepgram',
|
|
656
|
+
language: 'en',
|
|
657
|
+
sampleRate: 16000,
|
|
658
|
+
encoding: AudioEncoding.LINEAR16
|
|
659
|
+
},
|
|
660
|
+
onStateChange: stateChangeCallback
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
simplifiedClient.sendAudio(Buffer.from([1, 2, 3]));
|
|
664
|
+
|
|
665
|
+
const callbackState = stateChangeCallback.mock.calls[0][0];
|
|
666
|
+
const currentState = simplifiedClient.getVGFState();
|
|
667
|
+
|
|
668
|
+
expect(callbackState).not.toBe(currentState); // Different references
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
});
|