@volley/recognition-client-sdk-node22 0.1.424

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.
Files changed (58) hide show
  1. package/README.md +344 -0
  2. package/dist/browser.bundled.d.ts +1280 -0
  3. package/dist/browser.d.ts +10 -0
  4. package/dist/browser.d.ts.map +1 -0
  5. package/dist/config-builder.d.ts +134 -0
  6. package/dist/config-builder.d.ts.map +1 -0
  7. package/dist/errors.d.ts +41 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/factory.d.ts +36 -0
  10. package/dist/factory.d.ts.map +1 -0
  11. package/dist/index.bundled.d.ts +2572 -0
  12. package/dist/index.d.ts +16 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +10199 -0
  15. package/dist/index.js.map +7 -0
  16. package/dist/recog-client-sdk.browser.d.ts +10 -0
  17. package/dist/recog-client-sdk.browser.d.ts.map +1 -0
  18. package/dist/recog-client-sdk.browser.js +5746 -0
  19. package/dist/recog-client-sdk.browser.js.map +7 -0
  20. package/dist/recognition-client.d.ts +128 -0
  21. package/dist/recognition-client.d.ts.map +1 -0
  22. package/dist/recognition-client.types.d.ts +271 -0
  23. package/dist/recognition-client.types.d.ts.map +1 -0
  24. package/dist/simplified-vgf-recognition-client.d.ts +178 -0
  25. package/dist/simplified-vgf-recognition-client.d.ts.map +1 -0
  26. package/dist/utils/audio-ring-buffer.d.ts +69 -0
  27. package/dist/utils/audio-ring-buffer.d.ts.map +1 -0
  28. package/dist/utils/message-handler.d.ts +45 -0
  29. package/dist/utils/message-handler.d.ts.map +1 -0
  30. package/dist/utils/url-builder.d.ts +28 -0
  31. package/dist/utils/url-builder.d.ts.map +1 -0
  32. package/dist/vgf-recognition-mapper.d.ts +66 -0
  33. package/dist/vgf-recognition-mapper.d.ts.map +1 -0
  34. package/dist/vgf-recognition-state.d.ts +91 -0
  35. package/dist/vgf-recognition-state.d.ts.map +1 -0
  36. package/package.json +74 -0
  37. package/src/browser.ts +24 -0
  38. package/src/config-builder.spec.ts +265 -0
  39. package/src/config-builder.ts +240 -0
  40. package/src/errors.ts +84 -0
  41. package/src/factory.spec.ts +215 -0
  42. package/src/factory.ts +47 -0
  43. package/src/index.ts +127 -0
  44. package/src/recognition-client.spec.ts +889 -0
  45. package/src/recognition-client.ts +844 -0
  46. package/src/recognition-client.types.ts +338 -0
  47. package/src/simplified-vgf-recognition-client.integration.spec.ts +718 -0
  48. package/src/simplified-vgf-recognition-client.spec.ts +1525 -0
  49. package/src/simplified-vgf-recognition-client.ts +524 -0
  50. package/src/utils/audio-ring-buffer.spec.ts +335 -0
  51. package/src/utils/audio-ring-buffer.ts +170 -0
  52. package/src/utils/message-handler.spec.ts +311 -0
  53. package/src/utils/message-handler.ts +131 -0
  54. package/src/utils/url-builder.spec.ts +252 -0
  55. package/src/utils/url-builder.ts +92 -0
  56. package/src/vgf-recognition-mapper.spec.ts +78 -0
  57. package/src/vgf-recognition-mapper.ts +232 -0
  58. package/src/vgf-recognition-state.ts +102 -0
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Simplified VGF Recognition Client
3
+ *
4
+ * A thin wrapper around RealTimeTwoWayWebSocketRecognitionClient that maintains
5
+ * a VGF RecognitionState as a pure sink/output of recognition events.
6
+ *
7
+ * The VGF state is updated based on events but never influences client behavior.
8
+ * All functionality is delegated to the underlying client.
9
+ */
10
+
11
+ import {
12
+ RecognitionState,
13
+ TranscriptionStatus,
14
+ RecordingStatus,
15
+ RecognitionActionProcessingState
16
+ } from './vgf-recognition-state.js';
17
+ import {
18
+ IRecognitionClient,
19
+ IRecognitionClientConfig,
20
+ ClientState
21
+ } from './recognition-client.types.js';
22
+ import { RealTimeTwoWayWebSocketRecognitionClient } from './recognition-client.js';
23
+ import {
24
+ createVGFStateFromConfig,
25
+ mapTranscriptionResultToState,
26
+ mapErrorToState,
27
+ updateStateOnStop,
28
+ resetRecognitionVGFState
29
+ } from './vgf-recognition-mapper.js';
30
+ import { RecognitionContextTypeV1 } from '@recog/shared-types';
31
+
32
+ /**
33
+ * Configuration for SimplifiedVGFRecognitionClient
34
+ */
35
+ export interface SimplifiedVGFClientConfig extends IRecognitionClientConfig {
36
+ /**
37
+ * Callback invoked whenever the VGF state changes
38
+ * Use this to update your UI or React state
39
+ */
40
+ onStateChange?: (state: RecognitionState) => void;
41
+
42
+ /**
43
+ * Optional initial state to restore from a previous session
44
+ * If provided, audioUtteranceId will be extracted and used
45
+ */
46
+ initialState?: RecognitionState;
47
+ }
48
+
49
+ /**
50
+ * Interface for SimplifiedVGFRecognitionClient
51
+ *
52
+ * A simplified client that maintains VGF state for game developers.
53
+ * All methods from the underlying client are available, plus VGF state management.
54
+ */
55
+ export interface ISimplifiedVGFRecognitionClient {
56
+ // ============= Core Connection Methods =============
57
+ /**
58
+ * Connect to the recognition service WebSocket
59
+ * @returns Promise that resolves when connected and ready
60
+ */
61
+ connect(): Promise<void>;
62
+
63
+ /**
64
+ * Send audio data for transcription
65
+ * @param audioData - PCM audio data as ArrayBuffer, typed array, or Blob
66
+ */
67
+ sendAudio(audioData: ArrayBuffer | ArrayBufferView | Blob): void;
68
+
69
+ /**
70
+ * Stop recording and wait for final transcription
71
+ * @returns Promise that resolves when transcription is complete
72
+ */
73
+ stopRecording(): Promise<void>;
74
+
75
+ /**
76
+ * Force stop and immediately close connection without waiting for server
77
+ *
78
+ * WARNING: This is an abnormal shutdown that bypasses the graceful stop flow:
79
+ * - Does NOT wait for server to process remaining audio
80
+ * - Does NOT receive final transcript from server (VGF state set to empty)
81
+ * - Immediately closes WebSocket connection
82
+ * - Cleans up resources (buffers, listeners)
83
+ *
84
+ * Use Cases:
85
+ * - User explicitly cancels/abandons the session
86
+ * - Timeout scenarios where waiting is not acceptable
87
+ * - Need immediate cleanup and can't wait for server
88
+ *
89
+ * RECOMMENDED: Use stopRecording() for normal shutdown.
90
+ * Only use this when immediate disconnection is required.
91
+ */
92
+ stopAbnormally(): void;
93
+
94
+ // ============= VGF State Methods =============
95
+ /**
96
+ * Get the current VGF recognition state
97
+ * @returns Current RecognitionState with all transcription data
98
+ */
99
+ getVGFState(): RecognitionState;
100
+
101
+ // ============= Status Check Methods =============
102
+ /**
103
+ * Check if connected to the WebSocket
104
+ */
105
+ isConnected(): boolean;
106
+
107
+ /**
108
+ * Check if currently connecting
109
+ */
110
+ isConnecting(): boolean;
111
+
112
+ /**
113
+ * Check if currently stopping
114
+ */
115
+ isStopping(): boolean;
116
+
117
+ /**
118
+ * Check if transcription has finished
119
+ */
120
+ isTranscriptionFinished(): boolean;
121
+
122
+ /**
123
+ * Check if the audio buffer has overflowed
124
+ */
125
+ isBufferOverflowing(): boolean;
126
+
127
+ // ============= Utility Methods =============
128
+ /**
129
+ * Get the audio utterance ID for this session
130
+ */
131
+ getAudioUtteranceId(): string;
132
+
133
+ /**
134
+ * Get the WebSocket URL being used
135
+ */
136
+ getUrl(): string;
137
+
138
+ /**
139
+ * Get the underlying client state (for advanced usage)
140
+ */
141
+ getState(): ClientState;
142
+
143
+ }
144
+
145
+ /**
146
+ * This wrapper ONLY maintains VGF state as a sink.
147
+ * All actual functionality is delegated to the underlying client.
148
+ */
149
+ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognitionClient {
150
+ private client: IRecognitionClient;
151
+ private state: RecognitionState;
152
+ private isRecordingAudio: boolean = false;
153
+ private stateChangeCallback: ((state: RecognitionState) => void) | undefined;
154
+ private expectedUuid: string;
155
+ private logger: IRecognitionClientConfig['logger'];
156
+ private lastSentTerminalUuid: string | null = null;
157
+
158
+ constructor(config: SimplifiedVGFClientConfig) {
159
+ const { onStateChange, initialState, ...clientConfig } = config;
160
+ this.stateChangeCallback = onStateChange;
161
+ this.logger = clientConfig.logger;
162
+
163
+ // Use provided initial state or create from config
164
+ if (initialState) {
165
+ // Check if initial state has a valid UUID
166
+ const needsNewUuid = !initialState.audioUtteranceId ||
167
+ initialState.audioUtteranceId === '' ||
168
+ initialState.transcriptionStatus === TranscriptionStatus.ABORTED ||
169
+ initialState.transcriptionStatus === TranscriptionStatus.FINALIZED ||
170
+ initialState.transcriptionStatus === TranscriptionStatus.ERROR ||
171
+ (initialState.recognitionActionProcessingState !== undefined && initialState.recognitionActionProcessingState !== RecognitionActionProcessingState.COMPLETED);
172
+ if (needsNewUuid) {
173
+ // Reset session state with new UUID
174
+ this.state = resetRecognitionVGFState(initialState);
175
+ const newUUID = this.state.audioUtteranceId;
176
+
177
+ if (clientConfig.logger) {
178
+ const reason = !initialState.audioUtteranceId ? 'Missing UUID' :
179
+ initialState.audioUtteranceId === '' ? 'Empty UUID' :
180
+ `Terminal session (${initialState.transcriptionStatus})`;
181
+ clientConfig.logger('info', `${reason} detected, generating new UUID: ${newUUID}`);
182
+ }
183
+
184
+ // Use new UUID in client config
185
+ clientConfig.audioUtteranceId = newUUID;
186
+
187
+ // Reset terminal status tracking for new session
188
+ this.lastSentTerminalUuid = null;
189
+
190
+ // Notify state change immediately so app can update
191
+ if (onStateChange) {
192
+ onStateChange(this.state);
193
+ }
194
+ } else {
195
+ // Non-terminal state with valid UUID - safe to reuse (e.g., reconnecting to IN_PROGRESS session)
196
+ this.state = initialState;
197
+ // Override audioUtteranceId in config if state has one
198
+ if (initialState.audioUtteranceId && !clientConfig.audioUtteranceId) {
199
+ clientConfig.audioUtteranceId = initialState.audioUtteranceId;
200
+ }
201
+ }
202
+ } else {
203
+ // Initialize VGF state from config
204
+ this.state = createVGFStateFromConfig(clientConfig);
205
+ // Ensure clientConfig uses the same UUID as VGF state
206
+ clientConfig.audioUtteranceId = this.state.audioUtteranceId;
207
+ }
208
+
209
+ // Client is immediately ready to accept audio (will buffer if not connected)
210
+ this.state = { ...this.state, startRecordingStatus: 'READY' };
211
+
212
+ // Track the expected UUID for this session
213
+ this.expectedUuid = this.state.audioUtteranceId;
214
+
215
+ // If VGF state has promptSlotMap, configure gameContext to use it
216
+ if (this.state.promptSlotMap) {
217
+ // Set useContext=true in ASR config to enable context processing
218
+ if (clientConfig.asrRequestConfig) {
219
+ clientConfig.asrRequestConfig.useContext = true;
220
+ }
221
+
222
+ // Add promptSlotMap to gameContext
223
+ if (!clientConfig.gameContext) {
224
+ // Only create gameContext if we have gameId and gamePhase
225
+ // These should come from the game's configuration
226
+ if (clientConfig.logger) {
227
+ clientConfig.logger('warn', '[VGF] promptSlotMap found but no gameContext provided. SlotMap will not be sent.');
228
+ }
229
+ } else {
230
+ // Merge promptSlotMap into existing gameContext
231
+ clientConfig.gameContext.slotMap = this.state.promptSlotMap;
232
+ }
233
+ }
234
+
235
+ // Create underlying client with callbacks that ONLY update VGF state
236
+ this.client = new RealTimeTwoWayWebSocketRecognitionClient({
237
+ ...clientConfig,
238
+
239
+ // These callbacks ONLY update the VGF state sink
240
+ onTranscript: (result) => {
241
+ // Skip update if UUID doesn't match (stale callback from previous session)
242
+ if (result.audioUtteranceId && result.audioUtteranceId !== this.expectedUuid) {
243
+ if (this.logger) {
244
+ this.logger('warn',
245
+ `[RecogSDK:VGF] Skipping transcript update: UUID mismatch (expected: ${this.expectedUuid}, got: ${result.audioUtteranceId})`
246
+ );
247
+ }
248
+ return;
249
+ }
250
+
251
+ // Update VGF state based on transcript (always update local state)
252
+ this.state = mapTranscriptionResultToState(this.state, result, this.isRecordingAudio);
253
+ this.notifyStateChange();
254
+
255
+ // Call original callback if provided
256
+ if (clientConfig.onTranscript) {
257
+ clientConfig.onTranscript(result);
258
+ }
259
+ },
260
+
261
+ onMetadata: (metadata) => {
262
+ // Skip update if UUID doesn't match (stale callback from previous session)
263
+ if (metadata.audioUtteranceId && metadata.audioUtteranceId !== this.expectedUuid) {
264
+ if (this.logger) {
265
+ this.logger('warn',
266
+ `[RecogSDK:VGF] Skipping metadata update: UUID mismatch (expected: ${this.expectedUuid}, got: ${metadata.audioUtteranceId})`
267
+ );
268
+ }
269
+ return;
270
+ }
271
+
272
+ if (clientConfig.onMetadata) {
273
+ clientConfig.onMetadata(metadata);
274
+ }
275
+ },
276
+
277
+ onFunctionCall: (result) => {
278
+ // Pass through function call - no VGF state changes needed for P2 feature
279
+ if (clientConfig.onFunctionCall) {
280
+ clientConfig.onFunctionCall(result);
281
+ }
282
+ },
283
+
284
+ onError: (error) => {
285
+ // Skip update if UUID doesn't match (stale callback from previous session)
286
+ if (error.audioUtteranceId && error.audioUtteranceId !== this.expectedUuid) {
287
+ if (this.logger) {
288
+ this.logger('warn',
289
+ `[RecogSDK:VGF] Skipping error update: UUID mismatch (expected: ${this.expectedUuid}, got: ${error.audioUtteranceId})`
290
+ );
291
+ }
292
+ return;
293
+ }
294
+
295
+ this.isRecordingAudio = false; // Reset on error
296
+ this.state = mapErrorToState(this.state, error);
297
+ this.notifyStateChange();
298
+
299
+ if (clientConfig.onError) {
300
+ clientConfig.onError(error);
301
+ }
302
+ },
303
+
304
+ onConnected: () => {
305
+ // Don't update READY here - client can accept audio before connection
306
+ if (clientConfig.onConnected) {
307
+ clientConfig.onConnected();
308
+ }
309
+ },
310
+
311
+ onDisconnected: (code, reason) => {
312
+ this.isRecordingAudio = false; // Reset on disconnect
313
+ if (clientConfig.onDisconnected) {
314
+ clientConfig.onDisconnected(code, reason);
315
+ }
316
+ }
317
+ });
318
+ }
319
+
320
+ // DELEGATE ALL METHODS TO UNDERLYING CLIENT
321
+ // The wrapper ONLY updates VGF state, doesn't use it for decisions
322
+
323
+ async connect(): Promise<void> {
324
+ await this.client.connect();
325
+ // State will be updated via onConnected callback
326
+ }
327
+
328
+ sendAudio(audioData: ArrayBuffer | ArrayBufferView | Blob): void {
329
+ // Track recording for state updates
330
+ if (!this.isRecordingAudio) {
331
+ this.isRecordingAudio = true;
332
+ this.state = {
333
+ ...this.state,
334
+ startRecordingStatus: 'RECORDING',
335
+ startRecordingTimestamp: new Date().toISOString()
336
+ };
337
+ this.notifyStateChange();
338
+ }
339
+ this.client.sendAudio(audioData);
340
+ }
341
+
342
+ async stopRecording(): Promise<void> {
343
+ this.isRecordingAudio = false;
344
+ this.state = updateStateOnStop(this.state);
345
+ this.notifyStateChange();
346
+
347
+ // Early termination: If connection is not yet established with server, emit synthetic finalization immediately
348
+ // This prevents games from getting stuck waiting for a server response that may never come
349
+ if (this.client.getState() === ClientState.CONNECTED || this.client.getState() === ClientState.CONNECTING) {
350
+ if (this.logger) {
351
+ this.logger('info',
352
+ `[RecogSDK:VGF] Early termination detected (transcriptionStatus: NOT_STARTED) - emitting synthetic finalization`
353
+ );
354
+ }
355
+ this.state = {
356
+ ...this.state,
357
+ transcriptionStatus: TranscriptionStatus.FINALIZED,
358
+ finalTranscript: '',
359
+ finalConfidence: 0,
360
+ pendingTranscript: '',
361
+ pendingConfidence: undefined,
362
+ finalTranscriptionTimestamp: new Date().toISOString()
363
+ };
364
+ this.notifyStateChange();
365
+ }
366
+
367
+ await this.client.stopRecording();
368
+ }
369
+
370
+ stopAbnormally(): void {
371
+ const clientState = this.client.getState();
372
+
373
+ // Guard: Block if graceful shutdown in progress or already in terminal state
374
+ // This prevents stopAbnormally from disrupting stopRecording's graceful finalization
375
+ if (clientState === ClientState.STOPPING ||
376
+ clientState === ClientState.STOPPED ||
377
+ clientState === ClientState.FAILED) {
378
+ // Already stopping/stopped - do nothing to avoid disrupting graceful shutdown
379
+ return;
380
+ }
381
+
382
+ this.isRecordingAudio = false;
383
+
384
+ // Set state to ABORTED - preserve any partial transcript received so far
385
+ // This clearly indicates the session was cancelled/abandoned by user
386
+ if (this.state.transcriptionStatus !== TranscriptionStatus.ABORTED &&
387
+ this.state.transcriptionStatus !== TranscriptionStatus.FINALIZED) {
388
+ this.state = {
389
+ ...this.state,
390
+ transcriptionStatus: TranscriptionStatus.ABORTED,
391
+ startRecordingStatus: RecordingStatus.FINISHED,
392
+ finalRecordingTimestamp: new Date().toISOString(),
393
+ finalTranscriptionTimestamp: new Date().toISOString()
394
+ };
395
+ this.notifyStateChange();
396
+ }
397
+
398
+ // Delegate to underlying client for actual WebSocket cleanup
399
+ this.client.stopAbnormally();
400
+ }
401
+
402
+ // Pure delegation methods - no state logic
403
+ getAudioUtteranceId(): string {
404
+ return this.client.getAudioUtteranceId();
405
+ }
406
+
407
+ getUrl(): string {
408
+ return this.client.getUrl();
409
+ }
410
+
411
+ getState(): ClientState {
412
+ return this.client.getState();
413
+ }
414
+
415
+ isConnected(): boolean {
416
+ return this.client.isConnected();
417
+ }
418
+
419
+ isConnecting(): boolean {
420
+ return this.client.isConnecting();
421
+ }
422
+
423
+ isStopping(): boolean {
424
+ return this.client.isStopping();
425
+ }
426
+
427
+ isTranscriptionFinished(): boolean {
428
+ return this.client.isTranscriptionFinished();
429
+ }
430
+
431
+ isBufferOverflowing(): boolean {
432
+ return this.client.isBufferOverflowing();
433
+ }
434
+
435
+
436
+ // VGF State access (read-only for consumers)
437
+ getVGFState(): RecognitionState {
438
+ return { ...this.state };
439
+ }
440
+
441
+ private isTerminalStatus(status: string | undefined): boolean {
442
+ return status === TranscriptionStatus.FINALIZED ||
443
+ status === TranscriptionStatus.ABORTED ||
444
+ status === TranscriptionStatus.ERROR;
445
+ }
446
+
447
+ private notifyStateChange(): void {
448
+
449
+ // Block duplicate terminal status emissions for THIS session
450
+ if (this.isTerminalStatus(this.state.transcriptionStatus)) {
451
+ if (this.lastSentTerminalUuid === this.expectedUuid) {
452
+ // Already sent a terminal status for this session - suppress duplicate
453
+ if (this.logger) {
454
+ this.logger('info',
455
+ `[RecogSDK:VGF] Duplicate terminal status suppressed (lastSentTerminalUuid: ${this.lastSentTerminalUuid})`,
456
+ { transcriptionStatus: this.state.transcriptionStatus, finalTranscript: this.state.finalTranscript }
457
+ );
458
+ }
459
+ return;
460
+ }
461
+ // First terminal status for this session - record it
462
+ this.lastSentTerminalUuid = this.expectedUuid;
463
+ if (this.logger) {
464
+ this.logger('info',
465
+ `[RecogSDK:VGF] Sending terminal status (uuid: ${this.expectedUuid})`,
466
+ { transcriptionStatus: this.state.transcriptionStatus, finalTranscript: this.state.finalTranscript }
467
+ );
468
+ }
469
+ }
470
+
471
+ if (!this.stateChangeCallback) {
472
+ return;
473
+ }
474
+
475
+ this.stateChangeCallback({ ...this.state });
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Factory function for creating simplified client
481
+ * Usage examples:
482
+ *
483
+ * // Basic usage
484
+ * const client = createSimplifiedVGFClient({
485
+ * asrRequestConfig: { provider: 'deepgram', language: 'en' },
486
+ * onStateChange: (state) => {
487
+ * console.log('VGF State updated:', state);
488
+ * // Update React state, game UI, etc.
489
+ * }
490
+ * });
491
+ *
492
+ * // With initial state (e.g., restoring from previous session)
493
+ * const client = createSimplifiedVGFClient({
494
+ * asrRequestConfig: { provider: 'deepgram', language: 'en' },
495
+ * initialState: previousState, // Will use audioUtteranceId from state
496
+ * onStateChange: (state) => setVGFState(state)
497
+ * });
498
+ *
499
+ * // With initial state containing promptSlotMap for enhanced recognition
500
+ * const stateWithSlots: RecognitionState = {
501
+ * audioUtteranceId: 'session-123',
502
+ * promptSlotMap: {
503
+ * 'song_title': ['one time', 'baby'],
504
+ * 'artists': ['justin bieber']
505
+ * }
506
+ * };
507
+ * const client = createSimplifiedVGFClient({
508
+ * asrRequestConfig: { provider: 'deepgram', language: 'en' },
509
+ * gameContext: {
510
+ * type: RecognitionContextTypeV1.GAME_CONTEXT,
511
+ * gameId: 'music-quiz', // Your game's ID
512
+ * gamePhase: 'song-guessing' // Current game phase
513
+ * },
514
+ * initialState: stateWithSlots, // promptSlotMap will be added to gameContext
515
+ * onStateChange: (state) => setVGFState(state)
516
+ * });
517
+ *
518
+ * await client.connect();
519
+ * client.sendAudio(audioData);
520
+ * // VGF state automatically updates based on transcription results
521
+ */
522
+ export function createSimplifiedVGFClient(config: SimplifiedVGFClientConfig): ISimplifiedVGFRecognitionClient {
523
+ return new SimplifiedVGFRecognitionClient(config);
524
+ }