@webex/internal-plugin-voicea 2.59.2 → 2.59.3-next.1

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