@volley/recognition-client-sdk 0.1.424 → 0.1.622
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 +236 -7
- package/dist/index.bundled.d.ts +393 -52
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +268 -15
- package/dist/index.js.map +4 -4
- package/dist/recog-client-sdk.browser.js +236 -14
- package/dist/recog-client-sdk.browser.js.map +4 -4
- package/dist/recognition-client.d.ts +28 -1
- package/dist/recognition-client.d.ts.map +1 -1
- package/dist/recognition-client.types.d.ts +20 -0
- package/dist/recognition-client.types.d.ts.map +1 -1
- package/dist/simplified-vgf-recognition-client.d.ts +17 -0
- package/dist/simplified-vgf-recognition-client.d.ts.map +1 -1
- package/dist/vgf-recognition-mapper.d.ts.map +1 -1
- package/dist/vgf-recognition-state.d.ts +6 -0
- package/dist/vgf-recognition-state.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/index.ts +3 -0
- package/src/recognition-client.ts +158 -8
- package/src/recognition-client.types.ts +23 -0
- package/src/simplified-vgf-recognition-client.integration.spec.ts +15 -3
- package/src/simplified-vgf-recognition-client.ts +28 -1
- package/src/utils/audio-ring-buffer.spec.ts +335 -0
- package/src/vgf-recognition-mapper.ts +19 -1
- package/src/vgf-recognition-state.ts +4 -0
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
updateStateOnStop,
|
|
28
28
|
resetRecognitionVGFState
|
|
29
29
|
} from './vgf-recognition-mapper.js';
|
|
30
|
-
import { RecognitionContextTypeV1 } from '@recog/shared-types';
|
|
30
|
+
import { RecognitionContextTypeV1, type GameContextV1 } from '@recog/shared-types';
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Configuration for SimplifiedVGFRecognitionClient
|
|
@@ -124,6 +124,23 @@ export interface ISimplifiedVGFRecognitionClient {
|
|
|
124
124
|
*/
|
|
125
125
|
isBufferOverflowing(): boolean;
|
|
126
126
|
|
|
127
|
+
// ============= Preconnect Methods =============
|
|
128
|
+
/**
|
|
129
|
+
* Send game context after connection is established (for preconnect flow).
|
|
130
|
+
*
|
|
131
|
+
* Preconnect flow: Create client with asrRequestConfig (useContext: true) but
|
|
132
|
+
* WITHOUT gameContext → call connect() → later call sendGameContext() with slotMap.
|
|
133
|
+
*
|
|
134
|
+
* @param context - Game context including slotMap for keyword boosting
|
|
135
|
+
*/
|
|
136
|
+
sendGameContext(context: GameContextV1): void;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if server has sent READY signal (provider connected, ready for audio).
|
|
140
|
+
* In preconnect flow, this becomes true after sendGameContext() triggers provider attachment.
|
|
141
|
+
*/
|
|
142
|
+
isServerReady(): boolean;
|
|
143
|
+
|
|
127
144
|
// ============= Utility Methods =============
|
|
128
145
|
/**
|
|
129
146
|
* Get the audio utterance ID for this session
|
|
@@ -202,6 +219,8 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
202
219
|
} else {
|
|
203
220
|
// Initialize VGF state from config
|
|
204
221
|
this.state = createVGFStateFromConfig(clientConfig);
|
|
222
|
+
// Ensure clientConfig uses the same UUID as VGF state
|
|
223
|
+
clientConfig.audioUtteranceId = this.state.audioUtteranceId;
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
// Client is immediately ready to accept audio (will buffer if not connected)
|
|
@@ -430,6 +449,14 @@ export class SimplifiedVGFRecognitionClient implements ISimplifiedVGFRecognition
|
|
|
430
449
|
return this.client.isBufferOverflowing();
|
|
431
450
|
}
|
|
432
451
|
|
|
452
|
+
sendGameContext(context: GameContextV1): void {
|
|
453
|
+
this.client.sendGameContext(context);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
isServerReady(): boolean {
|
|
457
|
+
return this.client.isServerReady();
|
|
458
|
+
}
|
|
459
|
+
|
|
433
460
|
|
|
434
461
|
// VGF State access (read-only for consumers)
|
|
435
462
|
getVGFState(): RecognitionState {
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Ring Buffer Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AudioRingBuffer } from './audio-ring-buffer.js';
|
|
6
|
+
|
|
7
|
+
describe('AudioRingBuffer', () => {
|
|
8
|
+
describe('constructor', () => {
|
|
9
|
+
it('should initialize with correct buffer size', () => {
|
|
10
|
+
const buffer = new AudioRingBuffer({
|
|
11
|
+
maxBufferDurationSec: 10,
|
|
12
|
+
chunksPerSecond: 100
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(buffer.isEmpty()).toBe(true);
|
|
16
|
+
expect(buffer.getBufferedCount()).toBe(0);
|
|
17
|
+
expect(buffer.isOverflowing()).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should accept logger function', () => {
|
|
21
|
+
const logger = jest.fn();
|
|
22
|
+
const buffer = new AudioRingBuffer({
|
|
23
|
+
maxBufferDurationSec: 1,
|
|
24
|
+
chunksPerSecond: 10,
|
|
25
|
+
logger
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(buffer).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('write', () => {
|
|
33
|
+
it('should write ArrayBuffer data', () => {
|
|
34
|
+
const buffer = new AudioRingBuffer({
|
|
35
|
+
maxBufferDurationSec: 1,
|
|
36
|
+
chunksPerSecond: 10
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const data = new ArrayBuffer(100);
|
|
40
|
+
buffer.write(data);
|
|
41
|
+
|
|
42
|
+
expect(buffer.isEmpty()).toBe(false);
|
|
43
|
+
expect(buffer.getBufferedCount()).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should write ArrayBufferView data', () => {
|
|
47
|
+
const buffer = new AudioRingBuffer({
|
|
48
|
+
maxBufferDurationSec: 1,
|
|
49
|
+
chunksPerSecond: 10
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const data = new Uint8Array(100);
|
|
53
|
+
buffer.write(data);
|
|
54
|
+
|
|
55
|
+
expect(buffer.isEmpty()).toBe(false);
|
|
56
|
+
expect(buffer.getBufferedCount()).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should track total buffered bytes', () => {
|
|
60
|
+
const buffer = new AudioRingBuffer({
|
|
61
|
+
maxBufferDurationSec: 1,
|
|
62
|
+
chunksPerSecond: 10
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
buffer.write(new ArrayBuffer(100));
|
|
66
|
+
buffer.write(new ArrayBuffer(200));
|
|
67
|
+
|
|
68
|
+
const stats = buffer.getStats();
|
|
69
|
+
expect(stats.totalBufferedBytes).toBe(300);
|
|
70
|
+
expect(stats.chunksBuffered).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle multiple writes', () => {
|
|
74
|
+
const buffer = new AudioRingBuffer({
|
|
75
|
+
maxBufferDurationSec: 1,
|
|
76
|
+
chunksPerSecond: 100
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < 50; i++) {
|
|
80
|
+
buffer.write(new ArrayBuffer(10));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
expect(buffer.getBufferedCount()).toBe(50);
|
|
84
|
+
expect(buffer.isOverflowing()).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('overflow detection', () => {
|
|
89
|
+
it('should detect overflow when buffer is full', () => {
|
|
90
|
+
const buffer = new AudioRingBuffer({
|
|
91
|
+
maxBufferDurationSec: 1,
|
|
92
|
+
chunksPerSecond: 5 // Small buffer of 5 chunks
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Write more than buffer can hold
|
|
96
|
+
for (let i = 0; i < 10; i++) {
|
|
97
|
+
buffer.write(new ArrayBuffer(10));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
expect(buffer.isOverflowing()).toBe(true);
|
|
101
|
+
const stats = buffer.getStats();
|
|
102
|
+
expect(stats.hasWrapped).toBe(true);
|
|
103
|
+
expect(stats.overflowCount).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should call logger on overflow', () => {
|
|
107
|
+
const logger = jest.fn();
|
|
108
|
+
const buffer = new AudioRingBuffer({
|
|
109
|
+
maxBufferDurationSec: 1,
|
|
110
|
+
chunksPerSecond: 3, // Small buffer
|
|
111
|
+
logger
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Fill buffer to overflow
|
|
115
|
+
for (let i = 0; i < 10; i++) {
|
|
116
|
+
buffer.write(new ArrayBuffer(10));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Logger should have been called with overflow message
|
|
120
|
+
expect(logger).toHaveBeenCalledWith(
|
|
121
|
+
'debug',
|
|
122
|
+
expect.stringContaining('overflow'),
|
|
123
|
+
expect.any(Object)
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('read', () => {
|
|
129
|
+
it('should return null when buffer is empty', () => {
|
|
130
|
+
const buffer = new AudioRingBuffer({
|
|
131
|
+
maxBufferDurationSec: 1,
|
|
132
|
+
chunksPerSecond: 10
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(buffer.read()).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return data in FIFO order', () => {
|
|
139
|
+
const buffer = new AudioRingBuffer({
|
|
140
|
+
maxBufferDurationSec: 1,
|
|
141
|
+
chunksPerSecond: 10
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const data1 = new Uint8Array([1, 2, 3]);
|
|
145
|
+
const data2 = new Uint8Array([4, 5, 6]);
|
|
146
|
+
|
|
147
|
+
buffer.write(data1);
|
|
148
|
+
buffer.write(data2);
|
|
149
|
+
|
|
150
|
+
const chunk1 = buffer.read();
|
|
151
|
+
expect(chunk1).not.toBeNull();
|
|
152
|
+
expect(chunk1!.data).toBe(data1);
|
|
153
|
+
|
|
154
|
+
const chunk2 = buffer.read();
|
|
155
|
+
expect(chunk2).not.toBeNull();
|
|
156
|
+
expect(chunk2!.data).toBe(data2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should decrement buffered count after read', () => {
|
|
160
|
+
const buffer = new AudioRingBuffer({
|
|
161
|
+
maxBufferDurationSec: 1,
|
|
162
|
+
chunksPerSecond: 10
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
buffer.write(new ArrayBuffer(10));
|
|
166
|
+
buffer.write(new ArrayBuffer(10));
|
|
167
|
+
|
|
168
|
+
expect(buffer.getBufferedCount()).toBe(2);
|
|
169
|
+
|
|
170
|
+
buffer.read();
|
|
171
|
+
expect(buffer.getBufferedCount()).toBe(1);
|
|
172
|
+
|
|
173
|
+
buffer.read();
|
|
174
|
+
expect(buffer.getBufferedCount()).toBe(0);
|
|
175
|
+
expect(buffer.isEmpty()).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('readAll', () => {
|
|
180
|
+
it('should return empty array when buffer is empty', () => {
|
|
181
|
+
const buffer = new AudioRingBuffer({
|
|
182
|
+
maxBufferDurationSec: 1,
|
|
183
|
+
chunksPerSecond: 10
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(buffer.readAll()).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return all chunks without removing them', () => {
|
|
190
|
+
const buffer = new AudioRingBuffer({
|
|
191
|
+
maxBufferDurationSec: 1,
|
|
192
|
+
chunksPerSecond: 10
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
buffer.write(new ArrayBuffer(10));
|
|
196
|
+
buffer.write(new ArrayBuffer(20));
|
|
197
|
+
buffer.write(new ArrayBuffer(30));
|
|
198
|
+
|
|
199
|
+
const chunks = buffer.readAll();
|
|
200
|
+
expect(chunks).toHaveLength(3);
|
|
201
|
+
|
|
202
|
+
// Should still have all chunks
|
|
203
|
+
expect(buffer.getBufferedCount()).toBe(3);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('flush', () => {
|
|
208
|
+
it('should return all chunks and clear buffer', () => {
|
|
209
|
+
const buffer = new AudioRingBuffer({
|
|
210
|
+
maxBufferDurationSec: 1,
|
|
211
|
+
chunksPerSecond: 10
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
buffer.write(new ArrayBuffer(10));
|
|
215
|
+
buffer.write(new ArrayBuffer(20));
|
|
216
|
+
|
|
217
|
+
const chunks = buffer.flush();
|
|
218
|
+
expect(chunks).toHaveLength(2);
|
|
219
|
+
expect(buffer.isEmpty()).toBe(true);
|
|
220
|
+
expect(buffer.getBufferedCount()).toBe(0);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('clear', () => {
|
|
225
|
+
it('should reset all state', () => {
|
|
226
|
+
const buffer = new AudioRingBuffer({
|
|
227
|
+
maxBufferDurationSec: 1,
|
|
228
|
+
chunksPerSecond: 5
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Fill and overflow
|
|
232
|
+
for (let i = 0; i < 10; i++) {
|
|
233
|
+
buffer.write(new ArrayBuffer(100));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
expect(buffer.isOverflowing()).toBe(true);
|
|
237
|
+
|
|
238
|
+
buffer.clear();
|
|
239
|
+
|
|
240
|
+
expect(buffer.isEmpty()).toBe(true);
|
|
241
|
+
expect(buffer.getBufferedCount()).toBe(0);
|
|
242
|
+
expect(buffer.isOverflowing()).toBe(false);
|
|
243
|
+
|
|
244
|
+
const stats = buffer.getStats();
|
|
245
|
+
expect(stats.chunksBuffered).toBe(0);
|
|
246
|
+
expect(stats.totalBufferedBytes).toBe(0);
|
|
247
|
+
expect(stats.overflowCount).toBe(0);
|
|
248
|
+
expect(stats.hasWrapped).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should call logger when clearing', () => {
|
|
252
|
+
const logger = jest.fn();
|
|
253
|
+
const buffer = new AudioRingBuffer({
|
|
254
|
+
maxBufferDurationSec: 1,
|
|
255
|
+
chunksPerSecond: 10,
|
|
256
|
+
logger
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
buffer.write(new ArrayBuffer(10));
|
|
260
|
+
buffer.clear();
|
|
261
|
+
|
|
262
|
+
expect(logger).toHaveBeenCalledWith(
|
|
263
|
+
'debug',
|
|
264
|
+
expect.stringContaining('cleared')
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('getStats', () => {
|
|
270
|
+
it('should return correct statistics', () => {
|
|
271
|
+
const buffer = new AudioRingBuffer({
|
|
272
|
+
maxBufferDurationSec: 1,
|
|
273
|
+
chunksPerSecond: 10
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
buffer.write(new ArrayBuffer(100));
|
|
277
|
+
buffer.write(new ArrayBuffer(200));
|
|
278
|
+
|
|
279
|
+
const stats = buffer.getStats();
|
|
280
|
+
|
|
281
|
+
expect(stats.chunksBuffered).toBe(2);
|
|
282
|
+
expect(stats.currentBufferedChunks).toBe(2);
|
|
283
|
+
expect(stats.totalBufferedBytes).toBe(300);
|
|
284
|
+
expect(stats.hasWrapped).toBe(false);
|
|
285
|
+
expect(stats.overflowCount).toBe(0);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('getBufferedCount with wraparound', () => {
|
|
290
|
+
it('should correctly count after partial read and more writes', () => {
|
|
291
|
+
const buffer = new AudioRingBuffer({
|
|
292
|
+
maxBufferDurationSec: 1,
|
|
293
|
+
chunksPerSecond: 10 // Buffer size of 10
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Write 5 chunks
|
|
297
|
+
for (let i = 0; i < 5; i++) {
|
|
298
|
+
buffer.write(new ArrayBuffer(10));
|
|
299
|
+
}
|
|
300
|
+
expect(buffer.getBufferedCount()).toBe(5);
|
|
301
|
+
|
|
302
|
+
// Read 3 chunks
|
|
303
|
+
buffer.read();
|
|
304
|
+
buffer.read();
|
|
305
|
+
buffer.read();
|
|
306
|
+
expect(buffer.getBufferedCount()).toBe(2);
|
|
307
|
+
|
|
308
|
+
// Write 4 more (wraps around in buffer)
|
|
309
|
+
for (let i = 0; i < 4; i++) {
|
|
310
|
+
buffer.write(new ArrayBuffer(10));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Should have 2 + 4 = 6 chunks
|
|
314
|
+
expect(buffer.getBufferedCount()).toBe(6);
|
|
315
|
+
expect(buffer.isOverflowing()).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should handle buffer overflow correctly', () => {
|
|
319
|
+
const buffer = new AudioRingBuffer({
|
|
320
|
+
maxBufferDurationSec: 1,
|
|
321
|
+
chunksPerSecond: 3 // Buffer size of 3
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Write 5 chunks (more than buffer can hold)
|
|
325
|
+
for (let i = 0; i < 5; i++) {
|
|
326
|
+
buffer.write(new ArrayBuffer(10));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Buffer should be full but not exceed capacity
|
|
330
|
+
// Ring buffer drops oldest when full
|
|
331
|
+
expect(buffer.getBufferedCount()).toBeLessThanOrEqual(3);
|
|
332
|
+
expect(buffer.isOverflowing()).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -86,6 +86,14 @@ export function mapTranscriptionResultToState(
|
|
|
86
86
|
newState.finalConfidence = result.finalTranscriptConfidence;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
// Update voice timing on every transcript message
|
|
91
|
+
if (result.voiceEnd !== undefined) {
|
|
92
|
+
newState.voiceEnd = result.voiceEnd;
|
|
93
|
+
}
|
|
94
|
+
if (result.lastNonSilence !== undefined) {
|
|
95
|
+
newState.lastNonSilence = result.lastNonSilence;
|
|
96
|
+
}
|
|
89
97
|
} else {
|
|
90
98
|
// Transcription is finished
|
|
91
99
|
newState.transcriptionStatus = TranscriptionStatus.FINALIZED;
|
|
@@ -95,6 +103,14 @@ export function mapTranscriptionResultToState(
|
|
|
95
103
|
}
|
|
96
104
|
newState.finalTranscriptionTimestamp = new Date().toISOString();
|
|
97
105
|
|
|
106
|
+
// Update voice timing on final transcript
|
|
107
|
+
if (result.voiceEnd !== undefined) {
|
|
108
|
+
newState.voiceEnd = result.voiceEnd;
|
|
109
|
+
}
|
|
110
|
+
if (result.lastNonSilence !== undefined) {
|
|
111
|
+
newState.lastNonSilence = result.lastNonSilence;
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
// Clear pending when we have final
|
|
99
115
|
newState.pendingTranscript = "";
|
|
100
116
|
newState.pendingConfidence = undefined;
|
|
@@ -167,7 +183,9 @@ export function resetRecognitionVGFState(currentState: RecognitionState): Recogn
|
|
|
167
183
|
transcriptionStatus: TranscriptionStatus.NOT_STARTED,
|
|
168
184
|
startRecordingStatus: RecordingStatus.READY,
|
|
169
185
|
recognitionActionProcessingState: RecognitionActionProcessingState.NOT_STARTED,
|
|
170
|
-
finalTranscript: undefined
|
|
186
|
+
finalTranscript: undefined,
|
|
187
|
+
voiceEnd: undefined,
|
|
188
|
+
lastNonSilence: undefined
|
|
171
189
|
};
|
|
172
190
|
}
|
|
173
191
|
|
|
@@ -22,6 +22,10 @@ export const RecognitionVGFStateSchema = z.object({
|
|
|
22
22
|
finalTranscript: z.string().optional(), // Full finalized transcript for the utterance. Will not change.
|
|
23
23
|
finalConfidence: z.number().optional(),
|
|
24
24
|
|
|
25
|
+
// Voice timing (ms from stream start, prefix-adjusted)
|
|
26
|
+
voiceEnd: z.number().optional(), // voice end time identified by ASR
|
|
27
|
+
lastNonSilence: z.number().optional(), // last non-silence sample time from PCM analysis
|
|
28
|
+
|
|
25
29
|
// Tracking-only metadata
|
|
26
30
|
asrConfig: z.string().optional(), // Json format of the ASR config
|
|
27
31
|
startRecordingTimestamp: z.string().optional(), // Start of recording. Immutable after set.
|