@webex/internal-plugin-voicea 2.23.0

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.
@@ -0,0 +1,580 @@
1
+ import MockWebex from '@webex/test-helper-mock-webex';
2
+ import MockWebSocket from '@webex/test-helper-mock-web-socket';
3
+ import {assert} from '@webex/test-helper-chai';
4
+ import sinon from 'sinon';
5
+ import Mercury from '@webex/internal-plugin-mercury';
6
+ import LLMChannel from '@webex/internal-plugin-llm';
7
+ import {EVENT_TRIGGERS} from '@webex/internal-plugin-voicea/src/constants';
8
+ import VoiceaService from '@webex/internal-plugin-voicea';
9
+
10
+ describe('plugin-voicea', () => {
11
+ const locusUrl = 'locusUrl';
12
+ const datachannelUrl = 'datachannelUrl';
13
+
14
+ describe('voicea', () => {
15
+ let webex, voiceaService;
16
+
17
+
18
+ beforeEach(() => {
19
+ webex = new MockWebex({
20
+ children: {
21
+ mercury: Mercury,
22
+ llm: LLMChannel,
23
+ voicea: VoiceaService
24
+ },
25
+ });
26
+
27
+ voiceaService = webex.internal.voicea;
28
+ voiceaService.connect = sinon.stub().resolves(true);
29
+ voiceaService.request = sinon.stub().resolves({
30
+ headers: {},
31
+ body: ''
32
+ });
33
+ voiceaService.register = sinon.stub().resolves({
34
+ body: {
35
+ binding: 'binding',
36
+ webSocketUrl: 'url'
37
+ }
38
+ });
39
+ });
40
+
41
+ describe('#sendAnnouncement', () => {
42
+ beforeEach(async () => {
43
+ await voiceaService.registerAndConnect(locusUrl, datachannelUrl);
44
+ const mockWebSocket = new MockWebSocket();
45
+
46
+ voiceaService.socket = mockWebSocket;
47
+ });
48
+
49
+ it("sends announcement if voicea hasn't joined", () => {
50
+ voiceaService.sendAnnouncement();
51
+ assert.calledOnceWithExactly(voiceaService.socket.send, {
52
+ id: '1',
53
+ type: 'publishRequest',
54
+ recipients: {route: undefined},
55
+ headers: {},
56
+ data: {
57
+ clientPayload: {
58
+ version: 'v2',
59
+ },
60
+ eventType: 'relay.event',
61
+ relayType: 'client.annc',
62
+ },
63
+ trackingId: sinon.match.string
64
+ });
65
+ });
66
+ });
67
+
68
+ describe('#processAnnouncementMessage', () => {
69
+ it('works on non-empty payload', async () => {
70
+ const voiceaPayload = {
71
+ translation: {
72
+ allowed_languages: ['af', 'am'],
73
+ max_languages: 5,
74
+ },
75
+ ASR: {
76
+ spoken_languages: ['en']
77
+ },
78
+
79
+ version: 'v2'
80
+ };
81
+
82
+ const spy = sinon.spy();
83
+
84
+ voiceaService.on(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, spy);
85
+ voiceaService.processAnnouncementMessage(voiceaPayload);
86
+ assert.calledOnceWithExactly(spy, {
87
+ captionLanguages: ['af', 'am'],
88
+ spokenLanguages: ['en'],
89
+ maxLanguages: 5
90
+ });
91
+ });
92
+
93
+ it('works on non-empty payload', async () => {
94
+ const spy = sinon.spy();
95
+
96
+ voiceaService.on(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, spy);
97
+ await voiceaService.processAnnouncementMessage({});
98
+ assert.calledOnceWithExactly(spy, {
99
+ captionLanguages: [],
100
+ spokenLanguages: [],
101
+ maxLanguages: 0
102
+ });
103
+ });
104
+ });
105
+
106
+ describe('#requestLanguage', () => {
107
+ beforeEach(async () => {
108
+ await voiceaService.registerAndConnect(locusUrl, datachannelUrl);
109
+ const mockWebSocket = new MockWebSocket();
110
+
111
+ voiceaService.socket = mockWebSocket;
112
+ });
113
+
114
+ it('requests caption language', () => {
115
+ voiceaService.requestLanguage('en');
116
+
117
+ assert.calledOnceWithExactly(voiceaService.socket.send, {
118
+ id: '1',
119
+ type: 'publishRequest',
120
+ recipients: {route: undefined},
121
+ headers: {to: undefined},
122
+ data: {
123
+ clientPayload: {
124
+ translationLanguage: 'en',
125
+ id: sinon.match.string
126
+ },
127
+ eventType: 'relay.event',
128
+ relayType: 'voicea.transl.req'
129
+ },
130
+ trackingId: sinon.match.string
131
+ });
132
+ });
133
+ });
134
+
135
+ describe('#setSpokenLanguage', () => {
136
+ beforeEach(async () => {
137
+ await voiceaService.registerAndConnect(locusUrl, datachannelUrl);
138
+ });
139
+
140
+ it('sets spoken language', async () => {
141
+ const languageCode = 'en';
142
+ const triggerSpy = sinon.spy();
143
+
144
+ voiceaService.on(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, triggerSpy);
145
+ await voiceaService.setSpokenLanguage(languageCode);
146
+
147
+ assert.calledOnceWithExactly(triggerSpy, {languageCode});
148
+
149
+ sinon.assert.calledWith(voiceaService.request, sinon.match({
150
+ method: 'PUT',
151
+ url: `${locusUrl}/controls/`,
152
+ body: {languageCode}
153
+ }));
154
+ });
155
+ });
156
+
157
+ describe('#turnOnCaptions', () => {
158
+ beforeEach(async () => {
159
+ await voiceaService.registerAndConnect(locusUrl, datachannelUrl);
160
+
161
+ const mockWebSocket = new MockWebSocket();
162
+
163
+ voiceaService.socket = mockWebSocket;
164
+ });
165
+
166
+ it('turns on captions', async () => {
167
+ const announcementSpy = sinon.spy(voiceaService, 'sendAnnouncement');
168
+
169
+ const triggerSpy = sinon.spy();
170
+
171
+ voiceaService.on(EVENT_TRIGGERS.CAPTIONS_TURNED_ON, triggerSpy);
172
+ await voiceaService.turnOnCaptions();
173
+ sinon.assert.calledWith(voiceaService.request, sinon.match({
174
+ method: 'PUT',
175
+ url: `${locusUrl}/controls/`,
176
+ body: {transcribe: {caption: true}}
177
+ }));
178
+
179
+ assert.calledOnceWithExactly(triggerSpy, undefined);
180
+ assert.calledOnce(announcementSpy);
181
+ });
182
+
183
+ it('doesn\'t call API on captions', async () => {
184
+ await voiceaService.turnOnCaptions();
185
+
186
+ // eslint-disable-next-line no-underscore-dangle
187
+ voiceaService._emit('event:relay.event', {headers: {from: 'ws'}, voiceaPayload: {}, data: {relayType: 'voicea.annc'}});
188
+
189
+ const response = await voiceaService.turnOnCaptions();
190
+
191
+ assert.equal(response, undefined);
192
+ });
193
+ });
194
+
195
+ describe('#toggleTranscribing', () => {
196
+ beforeEach(async () => {
197
+ await voiceaService.registerAndConnect(locusUrl, datachannelUrl);
198
+
199
+ const mockWebSocket = new MockWebSocket();
200
+
201
+ voiceaService.socket = mockWebSocket;
202
+ });
203
+
204
+ it('turns on transcribing with CC enabled', async () => {
205
+ // Turn on captions
206
+ await voiceaService.turnOnCaptions();
207
+ const announcementSpy = sinon.spy(voiceaService, 'sendAnnouncement');
208
+
209
+ // eslint-disable-next-line no-underscore-dangle
210
+ voiceaService._emit('event:relay.event', {headers: {from: 'ws'}, voiceaPayload: {}, data: {relayType: 'voicea.annc'}});
211
+
212
+ const triggerSpy = sinon.spy();
213
+
214
+ voiceaService.on(EVENT_TRIGGERS.TRANSCRIBING_ON, triggerSpy);
215
+ await voiceaService.toggleTranscribing(true);
216
+ sinon.assert.calledWith(voiceaService.request, sinon.match({
217
+ method: 'PUT',
218
+ url: `${locusUrl}/controls/`,
219
+ body: {transcribe: {transcribing: true}}
220
+ }));
221
+
222
+ assert.calledOnce(triggerSpy);
223
+ assert.notCalled(announcementSpy);
224
+ });
225
+
226
+ it('turns on transcribing with CC disabled', async () => {
227
+ const announcementSpy = sinon.spy(voiceaService, 'sendAnnouncement');
228
+
229
+ const triggerSpy = sinon.spy();
230
+
231
+ voiceaService.on(EVENT_TRIGGERS.TRANSCRIBING_ON, triggerSpy);
232
+ await voiceaService.toggleTranscribing(true);
233
+ sinon.assert.calledWith(voiceaService.request, sinon.match({
234
+ method: 'PUT',
235
+ url: `${locusUrl}/controls/`,
236
+ body: {transcribe: {transcribing: true}}
237
+ }));
238
+
239
+ assert.calledOnce(triggerSpy);
240
+ assert.calledOnce(announcementSpy);
241
+ });
242
+
243
+ it('turns off transcribing', async () => {
244
+ await voiceaService.toggleTranscribing(true);
245
+
246
+ const announcementSpy = sinon.spy(voiceaService, 'sendAnnouncement');
247
+
248
+ const triggerSpy = sinon.spy();
249
+
250
+ voiceaService.on(EVENT_TRIGGERS.TRANSCRIBING_OFF, triggerSpy);
251
+ await voiceaService.toggleTranscribing(false);
252
+ sinon.assert.calledWith(voiceaService.request, sinon.match({
253
+ method: 'PUT',
254
+ url: `${locusUrl}/controls/`,
255
+ body: {transcribe: {transcribing: true}}
256
+ }));
257
+
258
+ assert.calledOnce(triggerSpy);
259
+ assert.notCalled(announcementSpy);
260
+ });
261
+
262
+ it('doesn\'t call API on same value', async () => {
263
+ await voiceaService.toggleTranscribing(true);
264
+ const triggerSpy = sinon.spy();
265
+ const announcementSpy = sinon.spy(voiceaService, 'sendAnnouncement');
266
+
267
+
268
+ voiceaService.on(EVENT_TRIGGERS.TRANSCRIBING_OFF, triggerSpy);
269
+
270
+ await voiceaService.toggleTranscribing(true);
271
+
272
+ assert.notCalled(triggerSpy);
273
+ assert.notCalled(announcementSpy);
274
+
275
+ sinon.assert.calledTwice(voiceaService.request);
276
+ });
277
+ });
278
+
279
+ describe('#processCaptionLanguageResponse', () => {
280
+ it('responds to process caption language', async () => {
281
+ const triggerSpy = sinon.spy();
282
+ const functionSpy = sinon.spy(voiceaService, 'processCaptionLanguageResponse');
283
+
284
+ voiceaService.on(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, triggerSpy);
285
+
286
+ // eslint-disable-next-line no-underscore-dangle
287
+ voiceaService._emit('event:relay.event', {
288
+ headers: {from: 'ws'},
289
+ voiceaPayload: {
290
+ statusCode: 200
291
+ },
292
+ data: {relayType: 'voicea.transl.rsp'}
293
+ });
294
+
295
+ assert.calledOnceWithExactly(triggerSpy, {statusCode: 200});
296
+ assert.calledOnce(functionSpy);
297
+ });
298
+ it('responds to process caption language for a failed response', async () => {
299
+ const triggerSpy = sinon.spy();
300
+ const functionSpy = sinon.spy(voiceaService, 'processCaptionLanguageResponse');
301
+
302
+ voiceaService.on(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, triggerSpy);
303
+
304
+ const payload = {
305
+ errorCode: 300,
306
+ message: 'error text'
307
+ };
308
+
309
+ // eslint-disable-next-line no-underscore-dangle
310
+ voiceaService._emit('event:relay.event', {
311
+ headers: {from: 'ws'},
312
+ voiceaPayload: payload,
313
+ data: {relayType: 'voicea.transl.rsp'}
314
+ });
315
+ assert.calledOnce(functionSpy);
316
+ assert.calledOnceWithExactly(triggerSpy, {statusCode: 300, errorMessage: 'error text'});
317
+ });
318
+ });
319
+
320
+ describe('#processTranscription', () => {
321
+ let triggerSpy, functionSpy;
322
+
323
+ beforeEach(() => {
324
+ triggerSpy = sinon.spy();
325
+ functionSpy = sinon.spy(voiceaService, 'processTranscription');
326
+ });
327
+
328
+ it('processes interim transcription', async () => {
329
+ voiceaService.on(EVENT_TRIGGERS.NEW_CAPTION, triggerSpy);
330
+
331
+ const voiceaPayload = {
332
+ audio_received_millis: 0,
333
+ command_response: '',
334
+ csis: [3556942592],
335
+ data: 'Hello.',
336
+ id: '38093ff5-f6a8-581c-9e59-035ec027994b',
337
+ meeting: '61d4e269-8419-42ab-9e56-3917974cda01',
338
+ transcript_id: '3ec73890-bffb-f28b-e77f-99dc13caea7e',
339
+ ts: 1611653204.3147924,
340
+ type: 'transcript_interim_results',
341
+
342
+ transcripts: [
343
+ {
344
+ text: 'Hello.',
345
+ csis: [
346
+ 3556942592
347
+ ],
348
+ transcript_language_code: 'en',
349
+ translations: {
350
+ fr: 'Bonjour.'
351
+ }
352
+ },
353
+ {
354
+ text: 'This is Webex',
355
+ csis: [
356
+ 3556942593
357
+ ],
358
+ transcript_language_code: 'en',
359
+ translations: {
360
+ fr: "C'est Webex"
361
+ }
362
+ }
363
+ ]
364
+
365
+ };
366
+
367
+ // eslint-disable-next-line no-underscore-dangle
368
+ await voiceaService._emit('event:relay.event', {
369
+ headers: {from: 'ws'},
370
+ voiceaPayload,
371
+ data: {relayType: 'voicea.transcription'}
372
+ });
373
+
374
+ assert.calledOnceWithExactly(functionSpy, voiceaPayload);
375
+ assert.calledOnceWithExactly(triggerSpy, {
376
+ isFinal: false, transcriptId: '3ec73890-bffb-f28b-e77f-99dc13caea7e', translations: undefined, transcript: {text: 'Hello.', csis: [3556942592]}
377
+ });
378
+ });
379
+
380
+ it('processes final transcription', async () => {
381
+ voiceaService.on(EVENT_TRIGGERS.NEW_CAPTION, triggerSpy);
382
+
383
+ const voiceaPayload = {
384
+ audio_received_millis: 0,
385
+ command_response: '',
386
+ csis: [3556942592],
387
+ data: 'Hello. This is Webex',
388
+ id: '38093ff5-f6a8-581c-9e59-035ec027994b',
389
+ meeting: '61d4e269-8419-42ab-9e56-3917974cda01',
390
+ transcript_id: '3ec73890-bffb-f28b-e77f-99dc13caea7e',
391
+ ts: 1611653204.3147924,
392
+ type: 'transcript_final_result',
393
+ transcript: {
394
+ alignments: [
395
+ {
396
+ end_millis: 12474,
397
+ start_millis: 12204,
398
+ word: 'Hello?'
399
+ }
400
+ ],
401
+ csis: [
402
+ 3556942592
403
+ ],
404
+ end_millis: 13044,
405
+ last_packet_timestamp_ms: 1611653206784,
406
+ start_millis: 12204,
407
+ text: 'Hello?',
408
+ transcript_language_code: 'en'
409
+ },
410
+ transcripts: [
411
+ {
412
+ start_millis: 12204,
413
+ end_millis: 13044,
414
+ text: 'Hello.',
415
+ csis: [
416
+ 3556942592
417
+ ],
418
+ transcript_language_code: 'en',
419
+ translations: {
420
+ fr: 'Bonjour.'
421
+ }
422
+ },
423
+ {
424
+ start_millis: 12204,
425
+ end_millis: 13044,
426
+ text: 'This is Webex',
427
+ csis: [
428
+ 3556942593
429
+ ],
430
+ transcript_language_code: 'en',
431
+ translations: {
432
+ fr: "C'est Webex"
433
+ }
434
+ }
435
+ ]
436
+
437
+ };
438
+
439
+ // eslint-disable-next-line no-underscore-dangle
440
+ await voiceaService._emit('event:relay.event', {
441
+ headers: {from: 'ws'},
442
+ voiceaPayload,
443
+ data: {relayType: 'voicea.transcription'}
444
+ });
445
+
446
+ assert.calledOnceWithExactly(functionSpy, voiceaPayload);
447
+ assert.calledOnceWithExactly(triggerSpy, {
448
+ isFinal: true, transcriptId: '3ec73890-bffb-f28b-e77f-99dc13caea7e', translations: undefined, timestamp: '0:01', transcript: {text: 'Hello. This is Webex', csis: [3556942592]}
449
+ });
450
+ });
451
+
452
+ it('processes a eva wake up', async () => {
453
+ voiceaService.on(EVENT_TRIGGERS.EVA_COMMAND, triggerSpy);
454
+
455
+ const voiceaPayload = {
456
+ audio_received_millis: 1616137504810,
457
+ command_response: '',
458
+ id: '31fb2f81-fb55-4257-32a0-f421ef8ba4b0',
459
+ meeting: 'fd5bd0fc-06fb-4fd1-982b-554c4368f101',
460
+ trigger: {
461
+ detected_at: '2021-03-19T07:05:04.810669662Z',
462
+ ews_confidence: 0.99497044086456299,
463
+ ews_keyphrase: 'OkayWebEx',
464
+ model_version: 'WebEx',
465
+ offset_seconds: 2336.5900000000001,
466
+ recording_file_name: 'OkayWebEx_fd5bd0fc-06fb-4fd1-982b-554c4368f101_47900f3f-8579-25eb-3f6a-74d81a3c66a4_2335.8900000000003_2336.79.raw',
467
+ type: 'live-hotword'
468
+ },
469
+ ts: 1616137504.8107769,
470
+ type: 'eva_wake'
471
+ };
472
+
473
+ // eslint-disable-next-line no-underscore-dangle
474
+ await voiceaService._emit('event:relay.event', {
475
+ headers: {from: 'ws'},
476
+ voiceaPayload,
477
+ data: {relayType: 'voicea.transcription'}
478
+ });
479
+
480
+ assert.calledOnceWithExactly(functionSpy, voiceaPayload);
481
+ assert.calledOnceWithExactly(triggerSpy, {
482
+ isListening: true
483
+ });
484
+ });
485
+
486
+ it('processes a eva thanks', async () => {
487
+ voiceaService.on(EVENT_TRIGGERS.EVA_COMMAND, triggerSpy);
488
+
489
+ const voiceaPayload = {
490
+ audio_received_millis: 0,
491
+ command_response: 'OK! Decision created.',
492
+ id: '9bc51440-1a22-7c81-6add-4b6ff7b59f7c',
493
+ intent: 'decision',
494
+ meeting: 'fd5bd0fc-06fb-4fd1-982b-554c4368f101',
495
+ ts: 1616135828.2552843,
496
+ type: 'eva_thanks'
497
+ };
498
+
499
+ // eslint-disable-next-line no-underscore-dangle
500
+ await voiceaService._emit('event:relay.event', {
501
+ headers: {from: 'ws'},
502
+ voiceaPayload,
503
+ data: {relayType: 'voicea.transcription'}
504
+ });
505
+
506
+ assert.calledOnceWithExactly(functionSpy, voiceaPayload);
507
+ assert.calledOnceWithExactly(triggerSpy, {
508
+ isListening: false, text: 'OK! Decision created.'
509
+ });
510
+ });
511
+
512
+ it('processes a eva cancel', async () => {
513
+ voiceaService.on(EVENT_TRIGGERS.EVA_COMMAND, triggerSpy);
514
+
515
+ const voiceaPayload = {
516
+ audio_received_millis: 0,
517
+ command_response: '',
518
+ id: '9bc51440-1a22-7c81-6add-4b6ff7b59f7c',
519
+ intent: 'decision',
520
+ meeting: 'fd5bd0fc-06fb-4fd1-982b-554c4368f101',
521
+ ts: 1616135828.2552843,
522
+ type: 'eva_cancel'
523
+ };
524
+
525
+ // eslint-disable-next-line no-underscore-dangle
526
+ await voiceaService._emit('event:relay.event', {
527
+ headers: {from: 'ws'},
528
+ voiceaPayload,
529
+ data: {relayType: 'voicea.transcription'}
530
+ });
531
+
532
+ assert.calledOnceWithExactly(functionSpy, voiceaPayload);
533
+ assert.calledOnceWithExactly(triggerSpy, {
534
+ isListening: false
535
+ });
536
+ });
537
+
538
+ it('processes a highlight', async () => {
539
+ voiceaService.on(EVENT_TRIGGERS.HIGHLIGHT_CREATED, triggerSpy);
540
+
541
+ const voiceaPayload = {
542
+ audio_received_millis: 0,
543
+ command_response: '',
544
+ highlight: {
545
+ created_by_email: '',
546
+ csis: [3932881920],
547
+ end_millis: 660160,
548
+ highlight_id: '219af4b1-1579-5106-53ab-f621094a0c5a',
549
+ highlight_label: 'Decision',
550
+ highlight_source: 'voice-command',
551
+ start_millis: 652756,
552
+ transcript: 'Create a decision to move ahead with the last proposal.',
553
+ trigger_info: {type: 'live-hotword'}
554
+ },
555
+ id: 'e6df0262-6289-db2e-581a-d44bb41b1c9c',
556
+ meeting: 'fd5bd0fc-06fb-4fd1-982b-554c4368f101',
557
+ ts: 1616135858.5349569,
558
+ type: 'highlight_created'
559
+ };
560
+
561
+ // eslint-disable-next-line no-underscore-dangle
562
+ await voiceaService._emit('event:relay.event', {
563
+ headers: {from: 'ws'},
564
+ voiceaPayload,
565
+ data: {relayType: 'voicea.transcription'}
566
+ });
567
+
568
+ assert.calledOnceWithExactly(functionSpy, voiceaPayload);
569
+ assert.calledOnceWithExactly(triggerSpy, {
570
+ csis: [3932881920],
571
+ highlightId: '219af4b1-1579-5106-53ab-f621094a0c5a',
572
+ text: 'Create a decision to move ahead with the last proposal.',
573
+ highlightLabel: 'Decision',
574
+ highlightSource: 'voice-command',
575
+ timestamp: '0:07'
576
+ });
577
+ });
578
+ });
579
+ });
580
+ });