@volley/recognition-client-sdk 0.1.385 → 0.1.418
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/dist/browser.bundled.d.ts +23 -1
- package/dist/config-builder.d.ts +5 -0
- package/dist/config-builder.d.ts.map +1 -1
- package/dist/index.bundled.d.ts +134 -80
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +136 -40
- package/dist/index.js.map +4 -4
- package/dist/recog-client-sdk.browser.js +62 -23
- package/dist/recog-client-sdk.browser.js.map +4 -4
- package/dist/recognition-client.d.ts.map +1 -1
- package/dist/recognition-client.types.d.ts +6 -0
- package/dist/recognition-client.types.d.ts.map +1 -1
- package/dist/simplified-vgf-recognition-client.d.ts +2 -0
- package/dist/simplified-vgf-recognition-client.d.ts.map +1 -1
- package/dist/utils/url-builder.d.ts +2 -0
- package/dist/utils/url-builder.d.ts.map +1 -1
- package/dist/vgf-recognition-mapper.d.ts +17 -0
- package/dist/vgf-recognition-mapper.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/config-builder.ts +9 -0
- package/src/index.ts +2 -0
- package/src/recognition-client.ts +3 -2
- package/src/recognition-client.types.ts +7 -0
- package/src/simplified-vgf-recognition-client.integration.spec.ts +704 -0
- package/src/simplified-vgf-recognition-client.spec.ts +199 -13
- package/src/simplified-vgf-recognition-client.ts +75 -24
- package/src/utils/audio-ring-buffer.ts +2 -2
- package/src/utils/message-handler.ts +4 -4
- package/src/utils/url-builder.ts +10 -3
- package/src/vgf-recognition-mapper.spec.ts +78 -0
- package/src/vgf-recognition-mapper.ts +29 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { SimplifiedVGFRecognitionClient, createSimplifiedVGFClient } from './simplified-vgf-recognition-client.js';
|
|
6
6
|
import { RealTimeTwoWayWebSocketRecognitionClient } from './recognition-client.js';
|
|
7
7
|
import { ClientState } from './recognition-client.types.js';
|
|
8
|
-
import { AudioEncoding, RecognitionContextTypeV1 } from '@recog/shared-types';
|
|
8
|
+
import { AudioEncoding, RecognitionContextTypeV1, RecognitionResultTypeV1 } from '@recog/shared-types';
|
|
9
9
|
import {
|
|
10
10
|
RecordingStatus,
|
|
11
11
|
TranscriptionStatus,
|
|
@@ -287,21 +287,43 @@ describe('SimplifiedVGFRecognitionClient', () => {
|
|
|
287
287
|
expect(updatedState.finalTranscriptionTimestamp).toBeDefined();
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
-
it('should
|
|
290
|
+
it('should pass metadata to callback without updating VGF state', () => {
|
|
291
291
|
// Get the actual UUID from the client
|
|
292
292
|
const actualUuid = simplifiedClient.getVGFState().audioUtteranceId;
|
|
293
|
+
const originalOnMetadata = jest.fn();
|
|
293
294
|
|
|
295
|
+
// Create new client with onMetadata callback
|
|
296
|
+
const clientWithMetadata = new SimplifiedVGFRecognitionClient({
|
|
297
|
+
asrRequestConfig: {
|
|
298
|
+
provider: 'deepgram',
|
|
299
|
+
language: 'en',
|
|
300
|
+
sampleRate: 16000,
|
|
301
|
+
encoding: AudioEncoding.LINEAR16
|
|
302
|
+
},
|
|
303
|
+
onStateChange: stateChangeCallback,
|
|
304
|
+
onMetadata: originalOnMetadata
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const constructorCalls = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls;
|
|
308
|
+
const latestConfig = constructorCalls[constructorCalls.length - 1]?.[0];
|
|
309
|
+
const metadataCallback = latestConfig?.onMetadata;
|
|
310
|
+
|
|
311
|
+
const clientUuid = clientWithMetadata.getVGFState().audioUtteranceId;
|
|
294
312
|
const metadata = {
|
|
295
|
-
|
|
296
|
-
|
|
313
|
+
type: RecognitionResultTypeV1.METADATA as const,
|
|
314
|
+
audioUtteranceId: clientUuid
|
|
297
315
|
};
|
|
298
316
|
|
|
299
|
-
|
|
317
|
+
// Clear previous state changes from client creation
|
|
318
|
+
stateChangeCallback.mockClear();
|
|
300
319
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
expect(
|
|
320
|
+
metadataCallback?.(metadata);
|
|
321
|
+
|
|
322
|
+
// Metadata should NOT trigger state change (state management simplified)
|
|
323
|
+
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
324
|
+
|
|
325
|
+
// But original callback should still be called
|
|
326
|
+
expect(originalOnMetadata).toHaveBeenCalledWith(metadata);
|
|
305
327
|
});
|
|
306
328
|
|
|
307
329
|
it('should handle errors and update state', () => {
|
|
@@ -325,13 +347,18 @@ describe('SimplifiedVGFRecognitionClient', () => {
|
|
|
325
347
|
// Then error occurs
|
|
326
348
|
onErrorCallback({ message: 'Error' });
|
|
327
349
|
|
|
328
|
-
//
|
|
350
|
+
// After terminal status (ERROR), no more state changes should be emitted
|
|
351
|
+
// The session is considered over, so sendAudio should not trigger callbacks
|
|
329
352
|
stateChangeCallback.mockClear();
|
|
330
353
|
simplifiedClient.sendAudio(Buffer.from([4, 5, 6]));
|
|
331
354
|
|
|
332
|
-
|
|
333
|
-
expect(
|
|
334
|
-
|
|
355
|
+
// No callback should be triggered since we're in terminal state
|
|
356
|
+
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
357
|
+
|
|
358
|
+
// However, the internal isRecordingAudio flag should still be reset
|
|
359
|
+
// (verified by the fact that the flag was set during the first sendAudio)
|
|
360
|
+
const state = simplifiedClient.getVGFState();
|
|
361
|
+
expect(state.transcriptionStatus).toBe(TranscriptionStatus.ERROR);
|
|
335
362
|
});
|
|
336
363
|
});
|
|
337
364
|
|
|
@@ -1331,4 +1358,163 @@ describe('SimplifiedVGFRecognitionClient', () => {
|
|
|
1331
1358
|
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
1332
1359
|
});
|
|
1333
1360
|
});
|
|
1361
|
+
|
|
1362
|
+
describe('Terminal Status Protection', () => {
|
|
1363
|
+
let stateChangeCallback: jest.Mock;
|
|
1364
|
+
let onTranscriptCallback: (result: any) => void;
|
|
1365
|
+
let onErrorCallback: (error: any) => void;
|
|
1366
|
+
let simplifiedClient: SimplifiedVGFRecognitionClient;
|
|
1367
|
+
let clientUuid: string;
|
|
1368
|
+
|
|
1369
|
+
beforeEach(() => {
|
|
1370
|
+
stateChangeCallback = jest.fn();
|
|
1371
|
+
simplifiedClient = new SimplifiedVGFRecognitionClient({
|
|
1372
|
+
asrRequestConfig: {
|
|
1373
|
+
provider: 'deepgram',
|
|
1374
|
+
language: 'en',
|
|
1375
|
+
sampleRate: 16000,
|
|
1376
|
+
encoding: AudioEncoding.LINEAR16
|
|
1377
|
+
},
|
|
1378
|
+
onStateChange: stateChangeCallback
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
const constructorCalls = (RealTimeTwoWayWebSocketRecognitionClient as jest.MockedClass<typeof RealTimeTwoWayWebSocketRecognitionClient>).mock.calls;
|
|
1382
|
+
const latestConfig = constructorCalls[constructorCalls.length - 1]?.[0];
|
|
1383
|
+
onTranscriptCallback = latestConfig?.onTranscript ?? jest.fn();
|
|
1384
|
+
onErrorCallback = latestConfig?.onError ?? jest.fn();
|
|
1385
|
+
clientUuid = simplifiedClient.getVGFState().audioUtteranceId;
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
it('should allow first terminal transcript callback', () => {
|
|
1389
|
+
onTranscriptCallback({
|
|
1390
|
+
type: 'Transcription',
|
|
1391
|
+
audioUtteranceId: clientUuid,
|
|
1392
|
+
finalTranscript: 'hello',
|
|
1393
|
+
finalTranscriptConfidence: 0.9,
|
|
1394
|
+
is_finished: true
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
expect(stateChangeCallback).toHaveBeenCalledTimes(1);
|
|
1398
|
+
expect(stateChangeCallback.mock.calls[0][0].transcriptionStatus).toBe(TranscriptionStatus.FINALIZED);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
it('should block second terminal transcript callback with same UUID', () => {
|
|
1402
|
+
// First terminal
|
|
1403
|
+
onTranscriptCallback({
|
|
1404
|
+
type: 'Transcription',
|
|
1405
|
+
audioUtteranceId: clientUuid,
|
|
1406
|
+
finalTranscript: 'first',
|
|
1407
|
+
finalTranscriptConfidence: 0.9,
|
|
1408
|
+
is_finished: true
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
stateChangeCallback.mockClear();
|
|
1412
|
+
|
|
1413
|
+
// Second terminal - should be blocked
|
|
1414
|
+
onTranscriptCallback({
|
|
1415
|
+
type: 'Transcription',
|
|
1416
|
+
audioUtteranceId: clientUuid,
|
|
1417
|
+
finalTranscript: 'second',
|
|
1418
|
+
finalTranscriptConfidence: 0.95,
|
|
1419
|
+
is_finished: true
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
it('should allow first error callback', () => {
|
|
1426
|
+
onErrorCallback({
|
|
1427
|
+
audioUtteranceId: clientUuid,
|
|
1428
|
+
message: 'Error occurred'
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
expect(stateChangeCallback).toHaveBeenCalledTimes(1);
|
|
1432
|
+
expect(stateChangeCallback.mock.calls[0][0].transcriptionStatus).toBe(TranscriptionStatus.ERROR);
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
it('should block second error callback with same UUID', () => {
|
|
1436
|
+
// First error
|
|
1437
|
+
onErrorCallback({
|
|
1438
|
+
audioUtteranceId: clientUuid,
|
|
1439
|
+
message: 'First error'
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
stateChangeCallback.mockClear();
|
|
1443
|
+
|
|
1444
|
+
// Second error - should be blocked
|
|
1445
|
+
onErrorCallback({
|
|
1446
|
+
audioUtteranceId: clientUuid,
|
|
1447
|
+
message: 'Second error'
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('should block transcript after error', () => {
|
|
1454
|
+
// Error first
|
|
1455
|
+
onErrorCallback({
|
|
1456
|
+
audioUtteranceId: clientUuid,
|
|
1457
|
+
message: 'Error'
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
stateChangeCallback.mockClear();
|
|
1461
|
+
|
|
1462
|
+
// Transcript after error - should be blocked
|
|
1463
|
+
onTranscriptCallback({
|
|
1464
|
+
type: 'Transcription',
|
|
1465
|
+
audioUtteranceId: clientUuid,
|
|
1466
|
+
finalTranscript: 'late',
|
|
1467
|
+
finalTranscriptConfidence: 0.9,
|
|
1468
|
+
is_finished: true
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
it('should block error after terminal transcript', () => {
|
|
1475
|
+
// Terminal transcript first
|
|
1476
|
+
onTranscriptCallback({
|
|
1477
|
+
type: 'Transcription',
|
|
1478
|
+
audioUtteranceId: clientUuid,
|
|
1479
|
+
finalTranscript: 'done',
|
|
1480
|
+
finalTranscriptConfidence: 0.9,
|
|
1481
|
+
is_finished: true
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
stateChangeCallback.mockClear();
|
|
1485
|
+
|
|
1486
|
+
// Error after terminal - should be blocked
|
|
1487
|
+
onErrorCallback({
|
|
1488
|
+
audioUtteranceId: clientUuid,
|
|
1489
|
+
message: 'Late error'
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
expect(stateChangeCallback).not.toHaveBeenCalled();
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
it('should preserve original state after blocking duplicate', () => {
|
|
1496
|
+
// First terminal with specific transcript
|
|
1497
|
+
onTranscriptCallback({
|
|
1498
|
+
type: 'Transcription',
|
|
1499
|
+
audioUtteranceId: clientUuid,
|
|
1500
|
+
finalTranscript: 'original',
|
|
1501
|
+
finalTranscriptConfidence: 0.85,
|
|
1502
|
+
is_finished: true
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// Attempt second terminal with different transcript
|
|
1506
|
+
onTranscriptCallback({
|
|
1507
|
+
type: 'Transcription',
|
|
1508
|
+
audioUtteranceId: clientUuid,
|
|
1509
|
+
finalTranscript: 'different',
|
|
1510
|
+
finalTranscriptConfidence: 0.99,
|
|
1511
|
+
is_finished: true
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// State should still have original values
|
|
1515
|
+
const state = simplifiedClient.getVGFState();
|
|
1516
|
+
expect(state.finalTranscript).toBe('original');
|
|
1517
|
+
expect(state.finalConfidence).toBe(0.85);
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1334
1520
|
});
|
|
@@ -24,10 +24,10 @@ import {
|
|
|
24
24
|
createVGFStateFromConfig,
|
|
25
25
|
mapTranscriptionResultToState,
|
|
26
26
|
mapErrorToState,
|
|
27
|
-
updateStateOnStop
|
|
27
|
+
updateStateOnStop,
|
|
28
|
+
resetSessionState
|
|
28
29
|
} from './vgf-recognition-mapper.js';
|
|
29
30
|
import { RecognitionContextTypeV1 } from '@recog/shared-types';
|
|
30
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Configuration for SimplifiedVGFRecognitionClient
|
|
@@ -153,6 +153,7 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
153
153
|
private stateChangeCallback: ((state: RecognitionState) => void) | undefined;
|
|
154
154
|
private expectedUuid: string;
|
|
155
155
|
private logger: IRecognitionClientConfig['logger'];
|
|
156
|
+
private lastSentTerminalUuid: string | null = null;
|
|
156
157
|
|
|
157
158
|
constructor(config: SimplifiedVGFClientConfig) {
|
|
158
159
|
const { onStateChange, initialState, ...clientConfig } = config;
|
|
@@ -169,8 +170,9 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
169
170
|
initialState.transcriptionStatus === TranscriptionStatus.ERROR ||
|
|
170
171
|
(initialState.recognitionActionProcessingState !== undefined && initialState.recognitionActionProcessingState !== RecognitionActionProcessingState.COMPLETED);
|
|
171
172
|
if (needsNewUuid) {
|
|
172
|
-
//
|
|
173
|
-
|
|
173
|
+
// Reset session state with new UUID
|
|
174
|
+
this.state = resetSessionState(initialState);
|
|
175
|
+
const newUUID = this.state.audioUtteranceId;
|
|
174
176
|
|
|
175
177
|
if (clientConfig.logger) {
|
|
176
178
|
const reason = !initialState.audioUtteranceId ? 'Missing UUID' :
|
|
@@ -179,19 +181,12 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
179
181
|
clientConfig.logger('info', `${reason} detected, generating new UUID: ${newUUID}`);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
// Update state with new UUID and reset session-specific fields
|
|
183
|
-
this.state = {
|
|
184
|
-
...initialState,
|
|
185
|
-
audioUtteranceId: newUUID,
|
|
186
|
-
transcriptionStatus: TranscriptionStatus.NOT_STARTED,
|
|
187
|
-
startRecordingStatus: RecordingStatus.READY,
|
|
188
|
-
recognitionActionProcessingState: RecognitionActionProcessingState.NOT_STARTED,
|
|
189
|
-
finalTranscript: undefined
|
|
190
|
-
};
|
|
191
|
-
|
|
192
184
|
// Use new UUID in client config
|
|
193
185
|
clientConfig.audioUtteranceId = newUUID;
|
|
194
186
|
|
|
187
|
+
// Reset terminal status tracking for new session
|
|
188
|
+
this.lastSentTerminalUuid = null;
|
|
189
|
+
|
|
195
190
|
// Notify state change immediately so app can update
|
|
196
191
|
if (onStateChange) {
|
|
197
192
|
onStateChange(this.state);
|
|
@@ -245,12 +240,19 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
245
240
|
if (result.audioUtteranceId && result.audioUtteranceId !== this.expectedUuid) {
|
|
246
241
|
if (this.logger) {
|
|
247
242
|
this.logger('warn',
|
|
248
|
-
`[VGF] Skipping transcript update: UUID mismatch (expected: ${this.expectedUuid}, got: ${result.audioUtteranceId})`
|
|
243
|
+
`[RecogSDK:VGF] Skipping transcript update: UUID mismatch (expected: ${this.expectedUuid}, got: ${result.audioUtteranceId})`
|
|
249
244
|
);
|
|
250
245
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Skip if terminal status already sent for THIS session
|
|
250
|
+
// (If lastSentTerminalUuid exists but doesn't match expectedUuid, treat as new session)
|
|
251
|
+
if (this.lastSentTerminalUuid === this.expectedUuid) {
|
|
252
|
+
if (this.logger) {
|
|
253
|
+
this.logger('info',
|
|
254
|
+
`[RecogSDK:VGF] Duplicate terminal status suppressed (lastSentTerminalUuid: ${this.lastSentTerminalUuid})`
|
|
255
|
+
);
|
|
254
256
|
}
|
|
255
257
|
return;
|
|
256
258
|
}
|
|
@@ -270,7 +272,7 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
270
272
|
if (metadata.audioUtteranceId && metadata.audioUtteranceId !== this.expectedUuid) {
|
|
271
273
|
if (this.logger) {
|
|
272
274
|
this.logger('warn',
|
|
273
|
-
`[VGF] Skipping metadata update: UUID mismatch (expected: ${this.expectedUuid}, got: ${metadata.audioUtteranceId})`
|
|
275
|
+
`[RecogSDK:VGF] Skipping metadata update: UUID mismatch (expected: ${this.expectedUuid}, got: ${metadata.audioUtteranceId})`
|
|
274
276
|
);
|
|
275
277
|
}
|
|
276
278
|
return;
|
|
@@ -293,12 +295,17 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
293
295
|
if (error.audioUtteranceId && error.audioUtteranceId !== this.expectedUuid) {
|
|
294
296
|
if (this.logger) {
|
|
295
297
|
this.logger('warn',
|
|
296
|
-
`[VGF] Skipping error update: UUID mismatch (expected: ${this.expectedUuid}, got: ${error.audioUtteranceId})`
|
|
298
|
+
`[RecogSDK:VGF] Skipping error update: UUID mismatch (expected: ${this.expectedUuid}, got: ${error.audioUtteranceId})`
|
|
297
299
|
);
|
|
298
300
|
}
|
|
299
301
|
return;
|
|
300
302
|
}
|
|
301
303
|
|
|
304
|
+
// Skip if terminal status already sent for THIS session
|
|
305
|
+
if (this.lastSentTerminalUuid === this.expectedUuid) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
302
309
|
this.isRecordingAudio = false; // Reset on error
|
|
303
310
|
this.state = mapErrorToState(this.state, error);
|
|
304
311
|
this.notifyStateChange();
|
|
@@ -350,6 +357,27 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
350
357
|
this.isRecordingAudio = false;
|
|
351
358
|
this.state = updateStateOnStop(this.state);
|
|
352
359
|
this.notifyStateChange();
|
|
360
|
+
|
|
361
|
+
// Early termination: If transcription never started (NOT_STARTED), emit synthetic finalization immediately
|
|
362
|
+
// This prevents games from getting stuck waiting for a server response that may never come
|
|
363
|
+
if (this.state.transcriptionStatus === TranscriptionStatus.NOT_STARTED) {
|
|
364
|
+
if (this.logger) {
|
|
365
|
+
this.logger('info',
|
|
366
|
+
`[RecogSDK:VGF] Early termination detected (transcriptionStatus: NOT_STARTED) - emitting synthetic finalization`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
this.state = {
|
|
370
|
+
...this.state,
|
|
371
|
+
transcriptionStatus: TranscriptionStatus.FINALIZED,
|
|
372
|
+
finalTranscript: '',
|
|
373
|
+
finalConfidence: 0,
|
|
374
|
+
pendingTranscript: '',
|
|
375
|
+
pendingConfidence: undefined,
|
|
376
|
+
finalTranscriptionTimestamp: new Date().toISOString()
|
|
377
|
+
};
|
|
378
|
+
this.notifyStateChange();
|
|
379
|
+
}
|
|
380
|
+
|
|
353
381
|
await this.client.stopRecording();
|
|
354
382
|
}
|
|
355
383
|
|
|
@@ -424,11 +452,34 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
424
452
|
return { ...this.state };
|
|
425
453
|
}
|
|
426
454
|
|
|
455
|
+
private isTerminalStatus(status: string | undefined): boolean {
|
|
456
|
+
return status === TranscriptionStatus.FINALIZED ||
|
|
457
|
+
status === TranscriptionStatus.ABORTED ||
|
|
458
|
+
status === TranscriptionStatus.ERROR;
|
|
459
|
+
}
|
|
460
|
+
|
|
427
461
|
private notifyStateChange(): void {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
462
|
+
|
|
463
|
+
// Block duplicate terminal status emissions for THIS session
|
|
464
|
+
if (this.isTerminalStatus(this.state.transcriptionStatus)) {
|
|
465
|
+
if (this.lastSentTerminalUuid === this.expectedUuid) {
|
|
466
|
+
// Already sent a terminal status for this session - suppress duplicate
|
|
467
|
+
if (this.logger) {
|
|
468
|
+
this.logger('info',
|
|
469
|
+
`[RecogSDK:VGF] Duplicate terminal status suppressed (lastSentTerminalUuid: ${this.lastSentTerminalUuid})`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// First terminal status for this session - record it
|
|
475
|
+
this.lastSentTerminalUuid = this.expectedUuid;
|
|
431
476
|
}
|
|
477
|
+
|
|
478
|
+
if (!this.stateChangeCallback) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
this.stateChangeCallback({ ...this.state });
|
|
432
483
|
}
|
|
433
484
|
}
|
|
434
485
|
|
|
@@ -477,4 +528,4 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
477
528
|
*/
|
|
478
529
|
export function createSimplifiedVGFClient(config: SimplifiedVGFClientConfig): ISimplifiedVGFRecognitionClient {
|
|
479
530
|
return new SimplifiedVGFRecognitionClient(config);
|
|
480
|
-
}
|
|
531
|
+
}
|
|
@@ -55,7 +55,7 @@ export class AudioRingBuffer {
|
|
|
55
55
|
|
|
56
56
|
// Log buffer overflow event
|
|
57
57
|
if (this.logger) {
|
|
58
|
-
this.logger('debug', 'Buffer overflow detected', {
|
|
58
|
+
this.logger('debug', '[RecogSDK] Buffer overflow detected', {
|
|
59
59
|
bufferSize: this.bufferSize,
|
|
60
60
|
totalOverflows: this.overflowCount,
|
|
61
61
|
droppedChunk: this.buffer[this.readIndex]?.timestamp
|
|
@@ -151,7 +151,7 @@ export class AudioRingBuffer {
|
|
|
151
151
|
this.totalBufferedBytes = 0;
|
|
152
152
|
|
|
153
153
|
if (this.logger) {
|
|
154
|
-
this.logger('debug', 'Audio buffer cleared');
|
|
154
|
+
this.logger('debug', '[RecogSDK] Audio buffer cleared');
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -44,7 +44,7 @@ export class MessageHandler {
|
|
|
44
44
|
handleMessage(msg: { v: number; type: string; data: any }): void {
|
|
45
45
|
// Log ALL incoming messages for debugging
|
|
46
46
|
if (this.callbacks.logger) {
|
|
47
|
-
this.callbacks.logger('debug', 'Received WebSocket message', {
|
|
47
|
+
this.callbacks.logger('debug', '[RecogSDK] Received WebSocket message', {
|
|
48
48
|
msgType: msg.type,
|
|
49
49
|
msgDataType: msg.data && typeof msg.data === 'object' && 'type' in msg.data ? msg.data.type : 'N/A',
|
|
50
50
|
fullMessage: msg
|
|
@@ -55,7 +55,7 @@ export class MessageHandler {
|
|
|
55
55
|
// Log error if we receive primitive data (indicates server issue)
|
|
56
56
|
if (msg.data && typeof msg.data !== 'object') {
|
|
57
57
|
if (this.callbacks.logger) {
|
|
58
|
-
this.callbacks.logger('error', 'Received primitive msg.data from server', {
|
|
58
|
+
this.callbacks.logger('error', '[RecogSDK] Received primitive msg.data from server', {
|
|
59
59
|
dataType: typeof msg.data,
|
|
60
60
|
data: msg.data,
|
|
61
61
|
fullMessage: msg
|
|
@@ -90,7 +90,7 @@ export class MessageHandler {
|
|
|
90
90
|
default:
|
|
91
91
|
// Unknown message type - log if logger available
|
|
92
92
|
if (this.callbacks.logger) {
|
|
93
|
-
this.callbacks.logger('debug', 'Unknown message type', { type: msgType });
|
|
93
|
+
this.callbacks.logger('debug', '[RecogSDK] Unknown message type', { type: msgType });
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -106,7 +106,7 @@ export class MessageHandler {
|
|
|
106
106
|
const timeToFirstTranscript = this.firstTranscriptTime - this.sessionStartTime;
|
|
107
107
|
|
|
108
108
|
if (this.callbacks.logger) {
|
|
109
|
-
this.callbacks.logger('debug', 'First transcript received', {
|
|
109
|
+
this.callbacks.logger('debug', '[RecogSDK] First transcript received', {
|
|
110
110
|
timeToFirstTranscriptMs: timeToFirstTranscript
|
|
111
111
|
});
|
|
112
112
|
}
|
package/src/utils/url-builder.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface UrlBuilderConfig {
|
|
|
19
19
|
questionAnswerId?: string;
|
|
20
20
|
platform?: string;
|
|
21
21
|
gameContext?: GameContextV1;
|
|
22
|
+
/** Standalone gameId - takes precedence over gameContext.gameId if both provided */
|
|
23
|
+
gameId?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -75,9 +77,14 @@ export function buildWebSocketUrl(config: UrlBuilderConfig): string {
|
|
|
75
77
|
url.searchParams.set('platform', config.platform);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
// Add gameId
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// Add gameId - standalone gameId takes precedence over gameContext.gameId
|
|
81
|
+
const gameId = config.gameId ?? config.gameContext?.gameId;
|
|
82
|
+
if (gameId) {
|
|
83
|
+
url.searchParams.set('gameId', gameId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Add gamePhase from gameContext if provided
|
|
87
|
+
if (config.gameContext?.gamePhase) {
|
|
81
88
|
url.searchParams.set('gamePhase', config.gameContext.gamePhase);
|
|
82
89
|
}
|
|
83
90
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for VGF Recognition Mapper functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
resetSessionState
|
|
7
|
+
} from './vgf-recognition-mapper.js';
|
|
8
|
+
import {
|
|
9
|
+
RecognitionState,
|
|
10
|
+
RecordingStatus,
|
|
11
|
+
TranscriptionStatus,
|
|
12
|
+
RecognitionActionProcessingState
|
|
13
|
+
} from './vgf-recognition-state.js';
|
|
14
|
+
|
|
15
|
+
describe('resetSessionState', () => {
|
|
16
|
+
it('should generate a new UUID', () => {
|
|
17
|
+
const originalState: RecognitionState = {
|
|
18
|
+
audioUtteranceId: 'old-uuid-123',
|
|
19
|
+
pendingTranscript: '',
|
|
20
|
+
transcriptionStatus: TranscriptionStatus.FINALIZED,
|
|
21
|
+
startRecordingStatus: RecordingStatus.FINISHED,
|
|
22
|
+
recognitionActionProcessingState: RecognitionActionProcessingState.COMPLETED,
|
|
23
|
+
finalTranscript: 'hello world'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const newState = resetSessionState(originalState);
|
|
27
|
+
|
|
28
|
+
expect(newState.audioUtteranceId).not.toBe('old-uuid-123');
|
|
29
|
+
expect(newState.audioUtteranceId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should reset session-specific fields', () => {
|
|
33
|
+
const originalState: RecognitionState = {
|
|
34
|
+
audioUtteranceId: 'old-uuid-123',
|
|
35
|
+
pendingTranscript: 'partial',
|
|
36
|
+
transcriptionStatus: TranscriptionStatus.FINALIZED,
|
|
37
|
+
startRecordingStatus: RecordingStatus.FINISHED,
|
|
38
|
+
recognitionActionProcessingState: RecognitionActionProcessingState.COMPLETED,
|
|
39
|
+
finalTranscript: 'hello world'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const newState = resetSessionState(originalState);
|
|
43
|
+
|
|
44
|
+
expect(newState.transcriptionStatus).toBe(TranscriptionStatus.NOT_STARTED);
|
|
45
|
+
expect(newState.startRecordingStatus).toBe(RecordingStatus.READY);
|
|
46
|
+
expect(newState.recognitionActionProcessingState).toBe(RecognitionActionProcessingState.NOT_STARTED);
|
|
47
|
+
expect(newState.finalTranscript).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should preserve non-session fields like promptSlotMap', () => {
|
|
51
|
+
const originalState: RecognitionState = {
|
|
52
|
+
audioUtteranceId: 'old-uuid-123',
|
|
53
|
+
pendingTranscript: '',
|
|
54
|
+
transcriptionStatus: TranscriptionStatus.FINALIZED,
|
|
55
|
+
promptSlotMap: { artist: ['taylor swift'], song: ['shake it off'] },
|
|
56
|
+
asrConfig: '{"provider":"deepgram"}'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const newState = resetSessionState(originalState);
|
|
60
|
+
|
|
61
|
+
expect(newState.promptSlotMap).toEqual({ artist: ['taylor swift'], song: ['shake it off'] });
|
|
62
|
+
expect(newState.asrConfig).toBe('{"provider":"deepgram"}');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should work with minimal state', () => {
|
|
66
|
+
const originalState: RecognitionState = {
|
|
67
|
+
audioUtteranceId: 'old-uuid',
|
|
68
|
+
pendingTranscript: ''
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const newState = resetSessionState(originalState);
|
|
72
|
+
|
|
73
|
+
expect(newState.audioUtteranceId).not.toBe('old-uuid');
|
|
74
|
+
expect(newState.transcriptionStatus).toBe(TranscriptionStatus.NOT_STARTED);
|
|
75
|
+
expect(newState.startRecordingStatus).toBe(RecordingStatus.READY);
|
|
76
|
+
expect(newState.recognitionActionProcessingState).toBe(RecognitionActionProcessingState.NOT_STARTED);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
RecognitionState,
|
|
10
10
|
RecordingStatus,
|
|
11
11
|
TranscriptionStatus,
|
|
12
|
+
RecognitionActionProcessingState,
|
|
12
13
|
createInitialRecognitionState
|
|
13
14
|
} from './vgf-recognition-state.js';
|
|
15
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
14
16
|
import {
|
|
15
17
|
ClientState,
|
|
16
18
|
IRecognitionClientConfig
|
|
@@ -142,6 +144,33 @@ export function updateStateOnStop(currentState: RecognitionState): RecognitionSt
|
|
|
142
144
|
};
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Resets session state with a new UUID.
|
|
149
|
+
*
|
|
150
|
+
* This creates a fresh session state while preserving non-session fields
|
|
151
|
+
* (like promptSlotMap, asrConfig, etc.)
|
|
152
|
+
*
|
|
153
|
+
* Resets:
|
|
154
|
+
* - audioUtteranceId → new UUID
|
|
155
|
+
* - transcriptionStatus → NOT_STARTED
|
|
156
|
+
* - startRecordingStatus → READY
|
|
157
|
+
* - recognitionActionProcessingState → NOT_STARTED
|
|
158
|
+
* - finalTranscript → undefined
|
|
159
|
+
*
|
|
160
|
+
* @param currentState - The current recognition state
|
|
161
|
+
* @returns A new state with reset session fields and a new UUID
|
|
162
|
+
*/
|
|
163
|
+
export function resetSessionState(currentState: RecognitionState): RecognitionState {
|
|
164
|
+
return {
|
|
165
|
+
...currentState,
|
|
166
|
+
audioUtteranceId: uuidv4(),
|
|
167
|
+
transcriptionStatus: TranscriptionStatus.NOT_STARTED,
|
|
168
|
+
startRecordingStatus: RecordingStatus.READY,
|
|
169
|
+
recognitionActionProcessingState: RecognitionActionProcessingState.NOT_STARTED,
|
|
170
|
+
finalTranscript: undefined
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
145
174
|
/**
|
|
146
175
|
* Updates state when client becomes ready
|
|
147
176
|
*/
|