@webex/internal-plugin-voicea 3.11.0 → 3.12.0-llmrefactor.2
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/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/voicea-plugin.d.ts +23 -0
- package/dist/types/voicea.d.ts +74 -19
- package/dist/types/voicea.types.d.ts +1 -0
- package/dist/voicea-plugin.js +59 -0
- package/dist/voicea-plugin.js.map +1 -0
- package/dist/voicea.js +259 -133
- package/dist/voicea.js.map +1 -1
- package/dist/voicea.types.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +5 -3
- package/src/voicea-plugin.ts +33 -0
- package/src/voicea.ts +241 -137
- package/src/voicea.types.ts +1 -0
- package/test/unit/spec/voicea-plugin.js +71 -0
- package/test/unit/spec/voicea.js +649 -842
package/test/unit/spec/voicea.js
CHANGED
|
@@ -4,70 +4,81 @@ import MockWebSocket from '@webex/test-helper-mock-web-socket';
|
|
|
4
4
|
import {assert, expect} from '@webex/test-helper-chai';
|
|
5
5
|
import sinon from 'sinon';
|
|
6
6
|
import Mercury from '@webex/internal-plugin-mercury';
|
|
7
|
-
import LLMChannel from '@webex/internal-plugin-llm';
|
|
8
7
|
|
|
9
|
-
import
|
|
8
|
+
import {VoiceaChannel} from '../../../src/voicea';
|
|
10
9
|
import {EVENT_TRIGGERS, TOGGLE_MANUAL_CAPTION_STATUS} from '../../../src/constants';
|
|
11
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Creates a mock LLM channel for testing
|
|
13
|
+
* @param {Object} [options] - Options for the mock channel
|
|
14
|
+
* @param {boolean} [options.isConnected=true] - Whether the channel is connected
|
|
15
|
+
* @param {string} [options.locusUrl] - The locus URL
|
|
16
|
+
* @returns {Object} Mock channel
|
|
17
|
+
*/
|
|
18
|
+
function createMockLLMChannel(options = {}) {
|
|
19
|
+
const mockWebSocket = new MockWebSocket();
|
|
20
|
+
const {isConnected = true, locusUrl = 'locusUrl'} = options;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
isConnected: sinon.stub().returns(isConnected),
|
|
24
|
+
getSocket: sinon.stub().returns(mockWebSocket),
|
|
25
|
+
getBinding: sinon.stub().returns('binding'),
|
|
26
|
+
getDatachannelUrl: sinon.stub().returns('datachannelUrl'),
|
|
27
|
+
getLocusUrl: sinon.stub().returns(locusUrl),
|
|
28
|
+
isDataChannelTokenEnabled: sinon.stub().resolves(true),
|
|
29
|
+
on: sinon.stub(),
|
|
30
|
+
off: sinon.stub(),
|
|
31
|
+
socket: mockWebSocket,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
describe('plugin-voicea', () => {
|
|
13
36
|
const locusUrl = 'locusUrl';
|
|
14
37
|
|
|
15
|
-
describe('
|
|
16
|
-
let webex,
|
|
38
|
+
describe('VoiceaChannel', () => {
|
|
39
|
+
let webex, voiceaChannel, mockLLMChannel, requestStub;
|
|
17
40
|
|
|
18
41
|
beforeEach(() => {
|
|
19
42
|
webex = new MockWebex({
|
|
20
43
|
children: {
|
|
21
44
|
mercury: Mercury,
|
|
22
|
-
llm: LLMChannel,
|
|
23
|
-
voicea: VoiceaService,
|
|
24
45
|
},
|
|
25
46
|
});
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
voiceaService.connect = sinon.stub().resolves(true);
|
|
29
|
-
voiceaService.webex.internal.llm.isConnected = sinon.stub().returns(true);
|
|
30
|
-
voiceaService.webex.internal.llm.getBinding = sinon.stub().returns(undefined);
|
|
31
|
-
voiceaService.webex.internal.llm.getLocusUrl = sinon.stub().returns(locusUrl);
|
|
32
|
-
|
|
33
|
-
voiceaService.request = sinon.stub().resolves({
|
|
48
|
+
requestStub = sinon.stub().resolves({
|
|
34
49
|
headers: {},
|
|
35
50
|
body: '',
|
|
36
51
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
webSocketUrl: 'url',
|
|
41
|
-
},
|
|
42
|
-
});
|
|
52
|
+
|
|
53
|
+
mockLLMChannel = createMockLLMChannel({locusUrl});
|
|
54
|
+
voiceaChannel = new VoiceaChannel(mockLLMChannel, requestStub);
|
|
43
55
|
});
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
assert.equal(voiceaService.captionStatus, 'idle');
|
|
49
|
-
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
voiceaChannel.deregisterEvents();
|
|
59
|
+
sinon.restore();
|
|
50
60
|
});
|
|
51
61
|
|
|
52
|
-
describe('#
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
describe('#constructor', () => {
|
|
63
|
+
it('should init status', () => {
|
|
64
|
+
assert.equal(voiceaChannel.getAnnounceStatus(), 'idle');
|
|
65
|
+
assert.equal(voiceaChannel.getCaptionStatus(), 'idle');
|
|
66
|
+
});
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
it('should subscribe to relay events', () => {
|
|
69
|
+
assert.calledWith(mockLLMChannel.on, 'event:relay.event', sinon.match.func);
|
|
58
70
|
});
|
|
71
|
+
});
|
|
59
72
|
|
|
73
|
+
describe('#sendAnnouncement', () => {
|
|
60
74
|
it("sends announcement if voicea hasn't joined", () => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
voiceaService.sendAnnouncement();
|
|
64
|
-
assert.equal(voiceaService.announceStatus, 'joining');
|
|
65
|
-
assert.calledOnce(spy);
|
|
75
|
+
voiceaChannel.sendAnnouncement();
|
|
76
|
+
assert.equal(voiceaChannel.getAnnounceStatus(), 'joining');
|
|
66
77
|
|
|
67
|
-
assert.calledOnceWithExactly(
|
|
78
|
+
assert.calledOnceWithExactly(mockLLMChannel.socket.send, {
|
|
68
79
|
id: '1',
|
|
69
80
|
type: 'publishRequest',
|
|
70
|
-
recipients: {route:
|
|
81
|
+
recipients: [{route: 'binding'}],
|
|
71
82
|
headers: {},
|
|
72
83
|
data: {
|
|
73
84
|
clientPayload: {
|
|
@@ -80,29 +91,15 @@ describe('plugin-voicea', () => {
|
|
|
80
91
|
});
|
|
81
92
|
});
|
|
82
93
|
|
|
83
|
-
it('listens to events once', () => {
|
|
84
|
-
const spy = sinon.spy(webex.internal.llm, 'on');
|
|
85
|
-
|
|
86
|
-
voiceaService.sendAnnouncement();
|
|
87
|
-
|
|
88
|
-
voiceaService.sendAnnouncement();
|
|
89
|
-
|
|
90
|
-
assert.calledOnceWithExactly(spy, 'event:relay.event', sinon.match.func);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
94
|
it('includes captionServiceId in headers when set', () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
97
|
-
voiceaService.announceStatus = 'idle';
|
|
98
|
-
voiceaService.captionServiceId = 'svc-123';
|
|
95
|
+
voiceaChannel.captionServiceId = 'svc-123';
|
|
99
96
|
|
|
100
|
-
|
|
97
|
+
voiceaChannel.sendAnnouncement();
|
|
101
98
|
|
|
102
|
-
assert.calledOnceWithExactly(
|
|
99
|
+
assert.calledOnceWithExactly(mockLLMChannel.socket.send, {
|
|
103
100
|
id: '1',
|
|
104
101
|
type: 'publishRequest',
|
|
105
|
-
recipients: {route:
|
|
102
|
+
recipients: [{route: 'binding'}],
|
|
106
103
|
headers: {to: 'svc-123'},
|
|
107
104
|
data: {
|
|
108
105
|
clientPayload: {
|
|
@@ -117,49 +114,38 @@ describe('plugin-voicea', () => {
|
|
|
117
114
|
});
|
|
118
115
|
|
|
119
116
|
describe('#sendManualClosedCaption', () => {
|
|
120
|
-
beforeEach(async () => {
|
|
121
|
-
const mockWebSocket = new MockWebSocket();
|
|
122
|
-
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
123
|
-
voiceaService.seqNum = 1;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
117
|
it('sends interim manual closed caption when connected', () => {
|
|
127
118
|
const text = 'Test interim caption';
|
|
128
119
|
const timeStamp = 1234567890;
|
|
129
120
|
const csis = [123456];
|
|
130
121
|
const isFinal = false;
|
|
131
122
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
assert.calledOnceWithExactly(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
],
|
|
155
|
-
transcript_id: sinon.match.string,
|
|
156
|
-
},
|
|
123
|
+
voiceaChannel.sendManualClosedCaption(text, timeStamp, csis, isFinal);
|
|
124
|
+
|
|
125
|
+
assert.calledOnceWithExactly(mockLLMChannel.socket.send, {
|
|
126
|
+
id: '1',
|
|
127
|
+
type: 'publishRequest',
|
|
128
|
+
recipients: [{route: 'binding'}],
|
|
129
|
+
headers: {},
|
|
130
|
+
data: {
|
|
131
|
+
eventType: 'relay.event',
|
|
132
|
+
relayType: 'client.manual_transcription',
|
|
133
|
+
transcriptPayload: {
|
|
134
|
+
type: 'manual_caption_interim_result',
|
|
135
|
+
id: sinon.match.string,
|
|
136
|
+
transcripts: [
|
|
137
|
+
{
|
|
138
|
+
text: 'Test interim caption',
|
|
139
|
+
start_millis: 1234567890,
|
|
140
|
+
end_millis: 1234567890,
|
|
141
|
+
csis: [123456],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
transcript_id: sinon.match.string,
|
|
157
145
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
// seqNum should increment
|
|
162
|
-
assert.equal(voiceaService.seqNum, 2);
|
|
146
|
+
},
|
|
147
|
+
trackingId: sinon.match.string,
|
|
148
|
+
});
|
|
163
149
|
});
|
|
164
150
|
|
|
165
151
|
it('sends final manual closed caption when connected', () => {
|
|
@@ -168,137 +154,74 @@ describe('plugin-voicea', () => {
|
|
|
168
154
|
const csis = [654321];
|
|
169
155
|
const isFinal = true;
|
|
170
156
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
assert.calledOnceWithExactly(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
],
|
|
194
|
-
transcript_id: sinon.match.string,
|
|
195
|
-
},
|
|
157
|
+
voiceaChannel.sendManualClosedCaption(text, timeStamp, csis, isFinal);
|
|
158
|
+
|
|
159
|
+
assert.calledOnceWithExactly(mockLLMChannel.socket.send, {
|
|
160
|
+
id: '1',
|
|
161
|
+
type: 'publishRequest',
|
|
162
|
+
recipients: [{route: 'binding'}],
|
|
163
|
+
headers: {},
|
|
164
|
+
data: {
|
|
165
|
+
eventType: 'relay.event',
|
|
166
|
+
relayType: 'client.manual_transcription',
|
|
167
|
+
transcriptPayload: {
|
|
168
|
+
type: 'manual_caption_final_result',
|
|
169
|
+
id: sinon.match.string,
|
|
170
|
+
transcripts: [
|
|
171
|
+
{
|
|
172
|
+
text: 'Test final caption',
|
|
173
|
+
start_millis: 9876543210,
|
|
174
|
+
end_millis: 9876543210,
|
|
175
|
+
csis: [654321],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
transcript_id: sinon.match.string,
|
|
196
179
|
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
);
|
|
200
|
-
// seqNum should increment
|
|
201
|
-
assert.equal(voiceaService.seqNum, 2);
|
|
180
|
+
},
|
|
181
|
+
trackingId: sinon.match.string,
|
|
182
|
+
});
|
|
202
183
|
});
|
|
203
184
|
|
|
204
185
|
it('does not send if not connected', () => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const text = 'Should not send';
|
|
208
|
-
const timeStamp = 111;
|
|
209
|
-
const csis = [1];
|
|
210
|
-
const isFinal = true;
|
|
186
|
+
const disconnectedChannel = createMockLLMChannel({isConnected: false});
|
|
187
|
+
const channel = new VoiceaChannel(disconnectedChannel, requestStub);
|
|
211
188
|
|
|
212
|
-
|
|
189
|
+
channel.sendManualClosedCaption('Should not send', 111, [1], true);
|
|
213
190
|
|
|
214
|
-
assert.notCalled(
|
|
191
|
+
assert.notCalled(disconnectedChannel.socket.send);
|
|
215
192
|
});
|
|
216
193
|
});
|
|
217
194
|
|
|
218
195
|
describe('#deregisterEvents', () => {
|
|
219
|
-
beforeEach(async () => {
|
|
220
|
-
const mockWebSocket = new MockWebSocket();
|
|
221
196
|
|
|
222
|
-
|
|
197
|
+
beforeEach(async () => {
|
|
198
|
+
voiceaChannel.keepTranscriptionSubscribed = true;
|
|
223
199
|
});
|
|
224
200
|
|
|
225
|
-
it('deregisters voicea
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
230
|
-
voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
231
|
-
headers: {from: 'ws'},
|
|
232
|
-
data: {relayType: 'voicea.annc', voiceaPayload: {}},
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
assert.equal(voiceaService.areCaptionsEnabled, true);
|
|
236
|
-
assert.equal(voiceaService.captionServiceId, 'ws');
|
|
237
|
-
|
|
238
|
-
voiceaService.deregisterEvents();
|
|
239
|
-
assert.equal(voiceaService.areCaptionsEnabled, false);
|
|
240
|
-
assert.equal(voiceaService.captionServiceId, undefined);
|
|
241
|
-
assert.equal(voiceaService.announceStatus, 'idle');
|
|
242
|
-
assert.equal(voiceaService.captionStatus, 'idle');
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
describe('#processAnnouncementMessage', () => {
|
|
246
|
-
it('works on non-empty payload', async () => {
|
|
247
|
-
const voiceaPayload = {
|
|
248
|
-
translation: {
|
|
249
|
-
allowed_languages: ['af', 'am'],
|
|
250
|
-
max_languages: 5,
|
|
251
|
-
},
|
|
252
|
-
ASR: {
|
|
253
|
-
spoken_languages: ['en'],
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
version: 'v2',
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const spy = sinon.spy();
|
|
260
|
-
|
|
261
|
-
voiceaService.on(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, spy);
|
|
262
|
-
voiceaService.listenToEvents();
|
|
263
|
-
voiceaService.processAnnouncementMessage(voiceaPayload);
|
|
264
|
-
assert.calledOnceWithExactly(spy, {
|
|
265
|
-
captionLanguages: ['af', 'am'],
|
|
266
|
-
spokenLanguages: ['en'],
|
|
267
|
-
maxLanguages: 5,
|
|
268
|
-
currentSpokenLanguage: 'en',
|
|
269
|
-
});
|
|
270
|
-
});
|
|
201
|
+
it('deregisters voicea channel and resets state', () => {
|
|
202
|
+
voiceaChannel.areCaptionsEnabled = true;
|
|
203
|
+
voiceaChannel.captionServiceId = 'ws';
|
|
204
|
+
voiceaChannel.keepTranscriptionSubscribed = true;
|
|
271
205
|
|
|
272
|
-
|
|
273
|
-
const spy = sinon.spy();
|
|
206
|
+
voiceaChannel.deregisterEvents();
|
|
274
207
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
assert.
|
|
280
|
-
|
|
281
|
-
spokenLanguages: [],
|
|
282
|
-
maxLanguages: 0,
|
|
283
|
-
currentSpokenLanguage: 'fr',
|
|
284
|
-
});
|
|
208
|
+
assert.equal(voiceaChannel.areCaptionsEnabled, false);
|
|
209
|
+
assert.equal(voiceaChannel.captionServiceId, undefined);
|
|
210
|
+
assert.equal(voiceaChannel.getAnnounceStatus(), 'idle');
|
|
211
|
+
assert.equal(voiceaChannel.getCaptionStatus(), 'idle');
|
|
212
|
+
assert.equal(voiceaChannel.keepTranscriptionSubscribed, false);
|
|
213
|
+
assert.calledWith(mockLLMChannel.off, 'event:relay.event', sinon.match.func);
|
|
285
214
|
});
|
|
286
215
|
});
|
|
287
216
|
|
|
288
217
|
describe('#requestLanguage', () => {
|
|
289
|
-
beforeEach(async () => {
|
|
290
|
-
const mockWebSocket = new MockWebSocket();
|
|
291
|
-
|
|
292
|
-
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
293
|
-
});
|
|
294
|
-
|
|
295
218
|
it('requests caption language', () => {
|
|
296
|
-
|
|
219
|
+
voiceaChannel.requestLanguage('en');
|
|
297
220
|
|
|
298
|
-
assert.calledOnceWithExactly(
|
|
221
|
+
assert.calledOnceWithExactly(mockLLMChannel.socket.send, {
|
|
299
222
|
id: '1',
|
|
300
223
|
type: 'publishRequest',
|
|
301
|
-
recipients: {route:
|
|
224
|
+
recipients: [{route: 'binding'}],
|
|
302
225
|
headers: {to: undefined},
|
|
303
226
|
data: {
|
|
304
227
|
clientPayload: {
|
|
@@ -313,14 +236,14 @@ describe('plugin-voicea', () => {
|
|
|
313
236
|
});
|
|
314
237
|
|
|
315
238
|
it('uses captionServiceId as "to" header when set', () => {
|
|
316
|
-
|
|
239
|
+
voiceaChannel.captionServiceId = 'svc-456';
|
|
317
240
|
|
|
318
|
-
|
|
241
|
+
voiceaChannel.requestLanguage('fr');
|
|
319
242
|
|
|
320
|
-
assert.calledOnceWithExactly(
|
|
243
|
+
assert.calledOnceWithExactly(mockLLMChannel.socket.send, {
|
|
321
244
|
id: '1',
|
|
322
245
|
type: 'publishRequest',
|
|
323
|
-
recipients: {route:
|
|
246
|
+
recipients: [{route: 'binding'}],
|
|
324
247
|
headers: {to: 'svc-456'},
|
|
325
248
|
data: {
|
|
326
249
|
clientPayload: {
|
|
@@ -333,6 +256,15 @@ describe('plugin-voicea', () => {
|
|
|
333
256
|
trackingId: sinon.match.string,
|
|
334
257
|
});
|
|
335
258
|
});
|
|
259
|
+
|
|
260
|
+
it('does not send when not connected', () => {
|
|
261
|
+
const disconnectedChannel = createMockLLMChannel({isConnected: false});
|
|
262
|
+
const channel = new VoiceaChannel(disconnectedChannel, requestStub);
|
|
263
|
+
|
|
264
|
+
channel.requestLanguage('en');
|
|
265
|
+
|
|
266
|
+
assert.notCalled(disconnectedChannel.socket.send);
|
|
267
|
+
});
|
|
336
268
|
});
|
|
337
269
|
|
|
338
270
|
describe('#setSpokenLanguage', () => {
|
|
@@ -340,38 +272,33 @@ describe('plugin-voicea', () => {
|
|
|
340
272
|
const languageCode = 'en';
|
|
341
273
|
const triggerSpy = sinon.spy();
|
|
342
274
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
await voiceaService.setSpokenLanguage(languageCode);
|
|
275
|
+
voiceaChannel.on(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, triggerSpy);
|
|
276
|
+
await voiceaChannel.setSpokenLanguage(languageCode);
|
|
346
277
|
|
|
347
278
|
assert.calledOnceWithExactly(triggerSpy, {languageCode});
|
|
348
279
|
|
|
349
280
|
sinon.assert.calledWith(
|
|
350
|
-
|
|
281
|
+
requestStub,
|
|
351
282
|
sinon.match({
|
|
352
283
|
method: 'PUT',
|
|
353
284
|
url: `${locusUrl}/controls/`,
|
|
354
285
|
body: {
|
|
355
286
|
transcribe: {
|
|
356
287
|
spokenLanguage: languageCode,
|
|
357
|
-
}
|
|
288
|
+
},
|
|
358
289
|
},
|
|
359
290
|
})
|
|
360
291
|
);
|
|
361
292
|
});
|
|
293
|
+
|
|
362
294
|
it('sets spoken language with language assignment', async () => {
|
|
363
295
|
const languageCode = 'zh';
|
|
364
296
|
const languageAssignment = 'DEFAULT';
|
|
365
|
-
const triggerSpy = sinon.spy();
|
|
366
297
|
|
|
367
|
-
|
|
368
|
-
voiceaService.listenToEvents();
|
|
369
|
-
await voiceaService.setSpokenLanguage(languageCode, languageAssignment);
|
|
370
|
-
|
|
371
|
-
assert.calledOnceWithExactly(triggerSpy, {languageCode});
|
|
298
|
+
await voiceaChannel.setSpokenLanguage(languageCode, languageAssignment);
|
|
372
299
|
|
|
373
300
|
sinon.assert.calledWith(
|
|
374
|
-
|
|
301
|
+
requestStub,
|
|
375
302
|
sinon.match({
|
|
376
303
|
method: 'PUT',
|
|
377
304
|
url: `${locusUrl}/controls/`,
|
|
@@ -379,831 +306,711 @@ describe('plugin-voicea', () => {
|
|
|
379
306
|
transcribe: {
|
|
380
307
|
spokenLanguage: languageCode,
|
|
381
308
|
languageAssignment,
|
|
382
|
-
}
|
|
309
|
+
},
|
|
383
310
|
},
|
|
384
311
|
})
|
|
385
312
|
);
|
|
386
313
|
});
|
|
387
|
-
|
|
388
314
|
});
|
|
389
315
|
|
|
390
|
-
describe('#
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
395
|
-
voiceaService.captionStatus = 'idle';
|
|
316
|
+
describe('#isLLMConnected', () => {
|
|
317
|
+
it('returns true when the LLM channel is connected', () => {
|
|
318
|
+
assert.equal(voiceaChannel.isLLMConnected(), true);
|
|
396
319
|
});
|
|
397
320
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
it('turns on captions', async () => {
|
|
403
|
-
const announcementSpy = sinon.spy(voiceaService, 'announce');
|
|
404
|
-
|
|
405
|
-
const triggerSpy = sinon.spy();
|
|
406
|
-
|
|
407
|
-
voiceaService.on(EVENT_TRIGGERS.CAPTIONS_TURNED_ON, triggerSpy);
|
|
408
|
-
voiceaService.listenToEvents();
|
|
409
|
-
|
|
410
|
-
await voiceaService.requestTurnOnCaptions();
|
|
411
|
-
assert.equal(voiceaService.captionStatus, 'enabled');
|
|
412
|
-
sinon.assert.calledWith(
|
|
413
|
-
voiceaService.request,
|
|
414
|
-
sinon.match({
|
|
415
|
-
method: 'PUT',
|
|
416
|
-
url: `${locusUrl}/controls/`,
|
|
417
|
-
body: {transcribe: {caption: true}},
|
|
418
|
-
})
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
assert.calledOnceWithExactly(triggerSpy);
|
|
422
|
-
|
|
423
|
-
assert.calledOnce(announcementSpy);
|
|
321
|
+
it('returns false when the LLM channel is not connected', () => {
|
|
322
|
+
mockLLMChannel.isConnected.returns(false);
|
|
323
|
+
assert.equal(voiceaChannel.isLLMConnected(), false);
|
|
424
324
|
});
|
|
325
|
+
});
|
|
425
326
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
327
|
+
describe('#getKeepTranscriptionSubscribed', () => {
|
|
328
|
+
it('returns false when keepTranscriptionSubscribed is false', () => {
|
|
329
|
+
voiceaChannel.keepTranscriptionSubscribed = false;
|
|
330
|
+
assert.equal(voiceaChannel.getKeepTranscriptionSubscribed(), false);
|
|
331
|
+
});
|
|
429
332
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
expect(error.message).to.include('turn on captions fail');
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
assert.equal(voiceaService.captionStatus, 'idle');
|
|
333
|
+
it('returns true when keepTranscriptionSubscribed is true', () => {
|
|
334
|
+
voiceaChannel.keepTranscriptionSubscribed = true;
|
|
335
|
+
assert.equal(voiceaChannel.getKeepTranscriptionSubscribed(), true);
|
|
437
336
|
});
|
|
438
337
|
});
|
|
439
338
|
|
|
440
|
-
describe(
|
|
441
|
-
|
|
442
|
-
|
|
339
|
+
describe('#announce', () => {
|
|
340
|
+
it('announce to llm data channel', () => {
|
|
341
|
+
const sendAnnouncementSpy = sinon.spy(voiceaChannel, 'sendAnnouncement');
|
|
342
|
+
voiceaChannel.announce();
|
|
343
|
+
assert.calledOnce(sendAnnouncementSpy);
|
|
443
344
|
});
|
|
444
345
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
346
|
+
it('throws when llm is not connected', () => {
|
|
347
|
+
mockLLMChannel.isConnected.returns(false);
|
|
348
|
+
assert.throws(
|
|
349
|
+
() => voiceaChannel.announce(),
|
|
350
|
+
'voicea can not announce before llm connected'
|
|
351
|
+
);
|
|
450
352
|
});
|
|
451
353
|
|
|
452
|
-
it('should
|
|
453
|
-
|
|
454
|
-
|
|
354
|
+
it('should not announce duplicate when already processed', () => {
|
|
355
|
+
voiceaChannel.announceStatus = 'joined';
|
|
356
|
+
const sendAnnouncementSpy = sinon.spy(voiceaChannel, 'sendAnnouncement');
|
|
357
|
+
voiceaChannel.announce();
|
|
358
|
+
assert.notCalled(sendAnnouncementSpy);
|
|
455
359
|
});
|
|
456
360
|
});
|
|
457
361
|
|
|
458
|
-
describe(
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
sendAnnouncement = sinon.stub(voiceaService, 'sendAnnouncement');
|
|
463
|
-
isAnnounceProcessing = sinon.stub(voiceaService, 'isAnnounceProcessing').returns(false)
|
|
464
|
-
});
|
|
362
|
+
describe('#turnOnCaptions', () => {
|
|
363
|
+
it('turns on captions', async () => {
|
|
364
|
+
const announceSpy = sinon.spy(voiceaChannel, 'announce');
|
|
365
|
+
const triggerSpy = sinon.spy();
|
|
465
366
|
|
|
466
|
-
|
|
467
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
468
|
-
isAnnounceProcessing.restore();
|
|
469
|
-
sendAnnouncement.restore();
|
|
470
|
-
});
|
|
367
|
+
voiceaChannel.on(EVENT_TRIGGERS.CAPTIONS_TURNED_ON, triggerSpy);
|
|
471
368
|
|
|
472
|
-
|
|
473
|
-
voiceaService.announce();
|
|
474
|
-
assert.calledOnce(sendAnnouncement);
|
|
475
|
-
});
|
|
369
|
+
await voiceaChannel.turnOnCaptions();
|
|
476
370
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
assert.
|
|
480
|
-
assert.notCalled(sendAnnouncement);
|
|
371
|
+
assert.equal(voiceaChannel.getCaptionStatus(), 'enabled');
|
|
372
|
+
assert.calledOnce(announceSpy);
|
|
373
|
+
assert.calledOnce(triggerSpy);
|
|
481
374
|
});
|
|
482
375
|
|
|
483
|
-
it('
|
|
484
|
-
|
|
485
|
-
voiceaService.announce();
|
|
486
|
-
assert.notCalled(sendAnnouncement);
|
|
487
|
-
})
|
|
488
|
-
});
|
|
376
|
+
it('throws when llm is not connected', async () => {
|
|
377
|
+
mockLLMChannel.isConnected.returns(false);
|
|
489
378
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
379
|
+
await assert.isRejected(
|
|
380
|
+
voiceaChannel.turnOnCaptions(),
|
|
381
|
+
'can not turn on captions before llm connected'
|
|
382
|
+
);
|
|
493
383
|
});
|
|
494
384
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
});
|
|
385
|
+
it('returns undefined when already sending', async () => {
|
|
386
|
+
voiceaChannel.captionStatus = 'sending';
|
|
387
|
+
const result = await voiceaChannel.turnOnCaptions();
|
|
388
|
+
assert.equal(result, undefined);
|
|
500
389
|
});
|
|
501
390
|
|
|
502
|
-
it('
|
|
503
|
-
|
|
504
|
-
assert.equal(voiceaService.isCaptionProcessing(), false);
|
|
505
|
-
});
|
|
506
|
-
});
|
|
391
|
+
it('throws error on request failure', async () => {
|
|
392
|
+
requestStub.rejects(new Error('Request failed'));
|
|
507
393
|
|
|
508
|
-
|
|
509
|
-
let requestTurnOnCaptions;
|
|
510
|
-
beforeEach(() => {
|
|
511
|
-
requestTurnOnCaptions = sinon.stub(voiceaService, 'requestTurnOnCaptions');
|
|
512
|
-
voiceaService.captionStatus = 'idle';
|
|
513
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
394
|
+
await assert.isRejected(voiceaChannel.turnOnCaptions(), 'turn on captions fail');
|
|
514
395
|
});
|
|
515
396
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
voiceaService.captionStatus = 'idle';
|
|
519
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
520
|
-
});
|
|
397
|
+
it('resets caption status to idle on error', async () => {
|
|
398
|
+
requestStub.rejects(new Error('Request failed'));
|
|
521
399
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
it("turns on captions before llm connected", () => {
|
|
529
|
-
voiceaService.captionStatus = 'idle';
|
|
530
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
531
|
-
// assert.throws(() => voiceaService.turnOnCaptions(), "can not turn on captions before llm connected");
|
|
532
|
-
assert.notCalled(requestTurnOnCaptions);
|
|
533
|
-
});
|
|
400
|
+
try {
|
|
401
|
+
await voiceaChannel.turnOnCaptions();
|
|
402
|
+
} catch {
|
|
403
|
+
// expected
|
|
404
|
+
}
|
|
534
405
|
|
|
535
|
-
|
|
536
|
-
voiceaService.captionStatus = 'sending';
|
|
537
|
-
voiceaService.turnOnCaptions();
|
|
538
|
-
assert.notCalled(voiceaService.requestTurnOnCaptions);
|
|
406
|
+
assert.equal(voiceaChannel.getCaptionStatus(), 'idle');
|
|
539
407
|
});
|
|
540
408
|
});
|
|
541
409
|
|
|
542
410
|
describe('#toggleTranscribing', () => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
it('turns on transcribing with CC enabled', async () => {
|
|
550
|
-
// Turn on captions
|
|
551
|
-
await voiceaService.turnOnCaptions();
|
|
552
|
-
const announcementSpy = sinon.spy(voiceaService, 'sendAnnouncement');
|
|
553
|
-
|
|
554
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
555
|
-
voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
556
|
-
headers: {from: 'ws'},
|
|
557
|
-
data: {relayType: 'voicea.annc', voiceaPayload: {}},
|
|
558
|
-
});
|
|
411
|
+
it('turns on transcribing', async () => {
|
|
412
|
+
await voiceaChannel.toggleTranscribing(true);
|
|
559
413
|
|
|
560
|
-
voiceaService.listenToEvents();
|
|
561
|
-
|
|
562
|
-
await voiceaService.toggleTranscribing(true);
|
|
563
414
|
sinon.assert.calledWith(
|
|
564
|
-
|
|
415
|
+
requestStub,
|
|
565
416
|
sinon.match({
|
|
566
417
|
method: 'PUT',
|
|
567
418
|
url: `${locusUrl}/controls/`,
|
|
568
419
|
body: {transcribe: {transcribing: true}},
|
|
569
420
|
})
|
|
570
421
|
);
|
|
571
|
-
|
|
572
|
-
assert.notCalled(announcementSpy);
|
|
573
422
|
});
|
|
574
423
|
|
|
575
|
-
it('turns
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
voiceaService.listenToEvents();
|
|
424
|
+
it('turns off transcribing', async () => {
|
|
425
|
+
await voiceaChannel.toggleTranscribing(false);
|
|
579
426
|
|
|
580
|
-
await voiceaService.toggleTranscribing(true);
|
|
581
427
|
sinon.assert.calledWith(
|
|
582
|
-
|
|
428
|
+
requestStub,
|
|
583
429
|
sinon.match({
|
|
584
430
|
method: 'PUT',
|
|
585
431
|
url: `${locusUrl}/controls/`,
|
|
586
|
-
body: {transcribe: {transcribing:
|
|
432
|
+
body: {transcribe: {transcribing: false}},
|
|
587
433
|
})
|
|
588
434
|
);
|
|
589
|
-
|
|
590
|
-
assert.calledOnce(announcementSpy);
|
|
591
435
|
});
|
|
592
436
|
|
|
593
|
-
it('
|
|
594
|
-
|
|
437
|
+
it('calls turnOnCaptions when activating and captions not enabled', async () => {
|
|
438
|
+
voiceaChannel.areCaptionsEnabled = false;
|
|
439
|
+
const turnOnCaptionsSpy = sinon.spy(voiceaChannel, 'turnOnCaptions');
|
|
595
440
|
|
|
596
|
-
|
|
441
|
+
await voiceaChannel.toggleTranscribing(true, 'en');
|
|
597
442
|
|
|
598
|
-
|
|
443
|
+
assert.calledOnceWithExactly(turnOnCaptionsSpy, 'en');
|
|
444
|
+
});
|
|
599
445
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
sinon.match({
|
|
604
|
-
method: 'PUT',
|
|
605
|
-
url: `${locusUrl}/controls/`,
|
|
606
|
-
body: {transcribe: {transcribing: true}},
|
|
607
|
-
})
|
|
608
|
-
);
|
|
446
|
+
it('does not call turnOnCaptions when captions already enabled', async () => {
|
|
447
|
+
voiceaChannel.areCaptionsEnabled = true;
|
|
448
|
+
const turnOnCaptionsSpy = sinon.spy(voiceaChannel, 'turnOnCaptions');
|
|
609
449
|
|
|
610
|
-
|
|
450
|
+
await voiceaChannel.toggleTranscribing(true);
|
|
451
|
+
|
|
452
|
+
assert.notCalled(turnOnCaptionsSpy);
|
|
611
453
|
});
|
|
612
454
|
});
|
|
613
455
|
|
|
614
456
|
describe('#toggleManualCaption', () => {
|
|
615
|
-
beforeEach(async () => {
|
|
616
|
-
const mockWebSocket = new MockWebSocket();
|
|
617
|
-
|
|
618
|
-
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
619
|
-
voiceaService.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.IDLE;
|
|
620
|
-
});
|
|
621
|
-
|
|
622
457
|
it('turns on manual caption', async () => {
|
|
623
|
-
await
|
|
458
|
+
await voiceaChannel.toggleManualCaption(true);
|
|
459
|
+
|
|
624
460
|
sinon.assert.calledWith(
|
|
625
|
-
|
|
461
|
+
requestStub,
|
|
626
462
|
sinon.match({
|
|
627
463
|
method: 'PUT',
|
|
628
464
|
url: `${locusUrl}/controls/`,
|
|
629
465
|
body: {manualCaption: {enable: true}},
|
|
630
466
|
})
|
|
631
467
|
);
|
|
632
|
-
|
|
633
468
|
});
|
|
634
469
|
|
|
635
|
-
|
|
636
470
|
it('turns off manual caption', async () => {
|
|
637
|
-
await
|
|
471
|
+
await voiceaChannel.toggleManualCaption(false);
|
|
472
|
+
|
|
638
473
|
sinon.assert.calledWith(
|
|
639
|
-
|
|
474
|
+
requestStub,
|
|
640
475
|
sinon.match({
|
|
641
476
|
method: 'PUT',
|
|
642
477
|
url: `${locusUrl}/controls/`,
|
|
643
478
|
body: {manualCaption: {enable: false}},
|
|
644
479
|
})
|
|
645
480
|
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('ignores when already sending', async () => {
|
|
484
|
+
voiceaChannel.toggleManualCaptionStatus = TOGGLE_MANUAL_CAPTION_STATUS.SENDING;
|
|
485
|
+
await voiceaChannel.toggleManualCaption(true);
|
|
486
|
+
sinon.assert.notCalled(requestStub);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('throws error on request failure', async () => {
|
|
490
|
+
requestStub.rejects(new Error('Request failed'));
|
|
646
491
|
|
|
492
|
+
await assert.isRejected(
|
|
493
|
+
voiceaChannel.toggleManualCaption(true),
|
|
494
|
+
'toggle manual captions fail'
|
|
495
|
+
);
|
|
647
496
|
});
|
|
648
497
|
|
|
649
|
-
it('
|
|
650
|
-
|
|
651
|
-
await voiceaService.toggleManualCaption(true);
|
|
498
|
+
it('resets status to idle on error', async () => {
|
|
499
|
+
requestStub.rejects(new Error('Request failed'));
|
|
652
500
|
|
|
653
|
-
|
|
501
|
+
try {
|
|
502
|
+
await voiceaChannel.toggleManualCaption(true);
|
|
503
|
+
} catch {
|
|
504
|
+
// expected
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
assert.equal(voiceaChannel.toggleManualCaptionStatus, TOGGLE_MANUAL_CAPTION_STATUS.IDLE);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
654
510
|
|
|
511
|
+
describe('#getCaptionStatus', () => {
|
|
512
|
+
it('returns current caption status', () => {
|
|
513
|
+
voiceaChannel.captionStatus = 'enabled';
|
|
514
|
+
assert.equal(voiceaChannel.getCaptionStatus(), 'enabled');
|
|
655
515
|
});
|
|
656
516
|
});
|
|
657
517
|
|
|
658
|
-
describe('#
|
|
659
|
-
it('
|
|
518
|
+
describe('#getAnnounceStatus', () => {
|
|
519
|
+
it('returns current announce status', () => {
|
|
520
|
+
voiceaChannel.announceStatus = 'joined';
|
|
521
|
+
assert.equal(voiceaChannel.getAnnounceStatus(), 'joined');
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe('#onSpokenLanguageUpdate', () => {
|
|
526
|
+
it('should trigger SPOKEN_LANGUAGE_UPDATE event', () => {
|
|
660
527
|
const triggerSpy = sinon.spy();
|
|
661
|
-
|
|
528
|
+
voiceaChannel.on(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, triggerSpy);
|
|
529
|
+
|
|
530
|
+
voiceaChannel.onSpokenLanguageUpdate('fr', '123');
|
|
531
|
+
|
|
532
|
+
assert.equal(voiceaChannel.currentSpokenLanguage, 'fr');
|
|
533
|
+
assert.calledOnceWithExactly(triggerSpy, {languageCode: 'fr', meetingId: '123'});
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe('#onCaptionServiceIdUpdate', () => {
|
|
538
|
+
it('does nothing when serviceId is falsy', () => {
|
|
539
|
+
voiceaChannel.captionServiceId = 'existing-id';
|
|
540
|
+
voiceaChannel.onCaptionServiceIdUpdate(undefined);
|
|
541
|
+
voiceaChannel.onCaptionServiceIdUpdate('');
|
|
542
|
+
assert.equal(voiceaChannel.captionServiceId, 'existing-id');
|
|
543
|
+
});
|
|
662
544
|
|
|
663
|
-
|
|
664
|
-
|
|
545
|
+
it('sets captionServiceId when no currentCaptionLanguage', () => {
|
|
546
|
+
voiceaChannel.captionServiceId = undefined;
|
|
547
|
+
voiceaChannel.currentCaptionLanguage = undefined;
|
|
548
|
+
voiceaChannel.onCaptionServiceIdUpdate('svc-new');
|
|
549
|
+
assert.equal(voiceaChannel.captionServiceId, 'svc-new');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('re-sends language when serviceId changes and currentCaptionLanguage is set', () => {
|
|
553
|
+
voiceaChannel.captionServiceId = 'old-svc';
|
|
554
|
+
voiceaChannel.currentCaptionLanguage = 'es';
|
|
555
|
+
|
|
556
|
+
voiceaChannel.onCaptionServiceIdUpdate('new-svc');
|
|
557
|
+
|
|
558
|
+
assert.equal(voiceaChannel.captionServiceId, 'new-svc');
|
|
559
|
+
assert.calledOnce(mockLLMChannel.socket.send);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe('#updateSubchannelSubscriptions', () => {
|
|
564
|
+
it('sends subchannelSubscriptionRequest', async () => {
|
|
565
|
+
await voiceaChannel.updateSubchannelSubscriptions({
|
|
566
|
+
subscribe: ['transcription'],
|
|
567
|
+
unsubscribe: ['polls'],
|
|
568
|
+
});
|
|
665
569
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
570
|
+
sinon.assert.calledOnceWithExactly(mockLLMChannel.socket.send, {
|
|
571
|
+
id: '1',
|
|
572
|
+
type: 'subchannelSubscriptionRequest',
|
|
669
573
|
data: {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
},
|
|
574
|
+
datachannelUri: 'datachannelUrl',
|
|
575
|
+
subscribe: ['transcription'],
|
|
576
|
+
unsubscribe: ['polls'],
|
|
674
577
|
},
|
|
578
|
+
trackingId: sinon.match.string,
|
|
675
579
|
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('does nothing when LLM is not connected', async () => {
|
|
583
|
+
mockLLMChannel.isConnected.returns(false);
|
|
584
|
+
|
|
585
|
+
await voiceaChannel.updateSubchannelSubscriptions({subscribe: ['transcription']});
|
|
676
586
|
|
|
677
|
-
assert.
|
|
678
|
-
assert.calledOnce(functionSpy);
|
|
587
|
+
sinon.assert.notCalled(mockLLMChannel.socket.send);
|
|
679
588
|
});
|
|
680
589
|
|
|
681
|
-
it('
|
|
682
|
-
|
|
683
|
-
const functionSpy = sinon.spy(voiceaService, 'processCaptionLanguageResponse');
|
|
590
|
+
it('does nothing when dataChannelToken is not enabled', async () => {
|
|
591
|
+
mockLLMChannel.isDataChannelTokenEnabled.resolves(false);
|
|
684
592
|
|
|
685
|
-
|
|
686
|
-
voiceaService.listenToEvents();
|
|
593
|
+
await voiceaChannel.updateSubchannelSubscriptions({subscribe: ['transcription']});
|
|
687
594
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
};
|
|
595
|
+
sinon.assert.notCalled(mockLLMChannel.socket.send);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
692
598
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
599
|
+
describe('#updateSubchannelSubscriptionsAndSyncCaptionState', () => {
|
|
600
|
+
it('updates caption intent and forwards to updateSubchannelSubscriptions', async () => {
|
|
601
|
+
const updateSpy = sinon.spy(voiceaChannel, 'updateSubchannelSubscriptions');
|
|
602
|
+
|
|
603
|
+
await voiceaChannel.updateSubchannelSubscriptionsAndSyncCaptionState(
|
|
604
|
+
{subscribe: ['transcription']},
|
|
605
|
+
true
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
assert.equal(voiceaChannel.getKeepTranscriptionSubscribed(), true);
|
|
609
|
+
assert.calledOnceWithExactly(updateSpy, {subscribe: ['transcription']});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('sets caption intent to false when isCCBoxOpen is false', async () => {
|
|
613
|
+
const updateSpy = sinon.spy(voiceaChannel, 'updateSubchannelSubscriptions');
|
|
614
|
+
|
|
615
|
+
await voiceaChannel.updateSubchannelSubscriptionsAndSyncCaptionState(
|
|
616
|
+
{subscribe: ['transcription']},
|
|
617
|
+
false
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
assert.equal(voiceaChannel.getKeepTranscriptionSubscribed(), false);
|
|
621
|
+
assert.calledOnceWithExactly(updateSpy, {subscribe: ['transcription']});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('defaults subscribe/unsubscribe to empty arrays when options is empty', async () => {
|
|
625
|
+
const updateSpy = sinon.spy(voiceaChannel, 'updateSubchannelSubscriptions');
|
|
626
|
+
|
|
627
|
+
await voiceaChannel.updateSubchannelSubscriptionsAndSyncCaptionState({}, true);
|
|
628
|
+
|
|
629
|
+
assert.equal(voiceaChannel.getKeepTranscriptionSubscribed(), true);
|
|
630
|
+
assert.calledOnceWithExactly(updateSpy, {});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('still updates caption intent even if updateSubchannelSubscriptions does nothing (e.g., LLM not connected)', async () => {
|
|
634
|
+
mockLLMChannel.isConnected.returns(false);
|
|
635
|
+
const updateSpy = sinon.spy(voiceaChannel, 'updateSubchannelSubscriptions');
|
|
636
|
+
|
|
637
|
+
await voiceaChannel.updateSubchannelSubscriptionsAndSyncCaptionState(
|
|
638
|
+
{subscribe: ['transcription']},
|
|
639
|
+
true
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
assert.equal(voiceaChannel.getKeepTranscriptionSubscribed(), true);
|
|
643
|
+
assert.calledOnceWithExactly(updateSpy, {subscribe: ['transcription']});
|
|
700
644
|
});
|
|
701
645
|
});
|
|
702
646
|
|
|
703
|
-
describe('
|
|
704
|
-
let
|
|
647
|
+
describe('event processor', () => {
|
|
648
|
+
let eventHandler;
|
|
705
649
|
|
|
706
650
|
beforeEach(() => {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
transcript_language_code: 'en',
|
|
727
|
-
translations: {
|
|
728
|
-
fr: "C'est Webex",
|
|
651
|
+
// Get the registered event handler
|
|
652
|
+
const onCall = mockLLMChannel.on
|
|
653
|
+
.getCalls()
|
|
654
|
+
.find((call) => call.args[0] === 'event:relay.event');
|
|
655
|
+
eventHandler = onCall?.args[1];
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('processes voicea announcement events', () => {
|
|
659
|
+
const spy = sinon.spy();
|
|
660
|
+
voiceaChannel.on(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, spy);
|
|
661
|
+
|
|
662
|
+
eventHandler({
|
|
663
|
+
sequenceNumber: 1,
|
|
664
|
+
headers: {from: 'ws-service'},
|
|
665
|
+
data: {
|
|
666
|
+
relayType: 'voicea.annc',
|
|
667
|
+
voiceaPayload: {
|
|
668
|
+
translation: {allowed_languages: ['en', 'es'], max_languages: 3},
|
|
669
|
+
ASR: {spoken_languages: ['en']},
|
|
729
670
|
},
|
|
730
671
|
},
|
|
731
|
-
];
|
|
732
|
-
const voiceaPayload = {
|
|
733
|
-
audio_received_millis: 0,
|
|
734
|
-
command_response: '',
|
|
735
|
-
csis: [3556942592],
|
|
736
|
-
data: 'Hello.',
|
|
737
|
-
id: '38093ff5-f6a8-581c-9e59-035ec027994b',
|
|
738
|
-
meeting: '61d4e269-8419-42ab-9e56-3917974cda01',
|
|
739
|
-
transcript_id: '3ec73890-bffb-f28b-e77f-99dc13caea7e',
|
|
740
|
-
ts: 1611653204.3147924,
|
|
741
|
-
type: 'transcript_interim_results',
|
|
742
|
-
|
|
743
|
-
transcripts,
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
747
|
-
await voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
748
|
-
headers: {from: 'ws'},
|
|
749
|
-
data: {relayType: 'voicea.transcription', voiceaPayload},
|
|
750
672
|
});
|
|
751
673
|
|
|
752
|
-
assert.
|
|
753
|
-
assert.calledOnceWithExactly(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
674
|
+
assert.equal(voiceaChannel.getAnnounceStatus(), 'joined');
|
|
675
|
+
assert.calledOnceWithExactly(spy, {
|
|
676
|
+
captionLanguages: ['en', 'es'],
|
|
677
|
+
maxLanguages: 3,
|
|
678
|
+
spokenLanguages: ['en'],
|
|
679
|
+
currentSpokenLanguage: 'en',
|
|
757
680
|
});
|
|
758
681
|
});
|
|
759
682
|
|
|
760
|
-
it('processes
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const voiceaPayload = {
|
|
764
|
-
audio_received_millis: 0,
|
|
765
|
-
command_response: '',
|
|
766
|
-
csis: [3556942592],
|
|
767
|
-
data: 'Hello. This is Webex',
|
|
768
|
-
id: '38093ff5-f6a8-581c-9e59-035ec027994b',
|
|
769
|
-
meeting: '61d4e269-8419-42ab-9e56-3917974cda01',
|
|
770
|
-
transcript_id: '3ec73890-bffb-f28b-e77f-99dc13caea7e',
|
|
771
|
-
ts: 1611653204.3147924,
|
|
772
|
-
type: 'transcript_final_result',
|
|
773
|
-
translations: {
|
|
774
|
-
en: "Hello?",
|
|
775
|
-
},
|
|
776
|
-
transcript: {
|
|
777
|
-
alignments: [
|
|
778
|
-
{
|
|
779
|
-
end_millis: 12474,
|
|
780
|
-
start_millis: 12204,
|
|
781
|
-
word: 'Hello?',
|
|
782
|
-
},
|
|
783
|
-
],
|
|
784
|
-
csis: [3556942592],
|
|
785
|
-
end_millis: 13044,
|
|
786
|
-
last_packet_timestamp_ms: 1611653206784,
|
|
787
|
-
start_millis: 12204,
|
|
788
|
-
text: 'Hello?',
|
|
789
|
-
transcript_language_code: 'en',
|
|
790
|
-
timestamp: '0:13'
|
|
791
|
-
},
|
|
792
|
-
transcripts: [
|
|
793
|
-
{
|
|
794
|
-
start_millis: 12204,
|
|
795
|
-
end_millis: 13044,
|
|
796
|
-
text: 'Hello.',
|
|
797
|
-
csis: [3556942592],
|
|
798
|
-
transcript_language_code: 'en',
|
|
799
|
-
translations: {
|
|
800
|
-
fr: 'Bonjour.',
|
|
801
|
-
},
|
|
802
|
-
timestamp: '0:13'
|
|
803
|
-
},
|
|
804
|
-
{
|
|
805
|
-
start_millis: 12204,
|
|
806
|
-
end_millis: 13044,
|
|
807
|
-
text: 'This is Webex',
|
|
808
|
-
csis: [3556942593],
|
|
809
|
-
transcript_language_code: 'en',
|
|
810
|
-
translations: {
|
|
811
|
-
fr: "C'est Webex",
|
|
812
|
-
},
|
|
813
|
-
timestamp: '0:13'
|
|
814
|
-
},
|
|
815
|
-
],
|
|
816
|
-
};
|
|
683
|
+
it('processes voicea announcement with empty payload', () => {
|
|
684
|
+
const spy = sinon.spy();
|
|
685
|
+
voiceaChannel.on(EVENT_TRIGGERS.VOICEA_ANNOUNCEMENT, spy);
|
|
817
686
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
headers: {from: 'ws'},
|
|
821
|
-
data: {
|
|
687
|
+
eventHandler({
|
|
688
|
+
sequenceNumber: 1,
|
|
689
|
+
headers: {from: 'ws-service'},
|
|
690
|
+
data: {
|
|
691
|
+
relayType: 'voicea.annc',
|
|
692
|
+
voiceaPayload: {},
|
|
693
|
+
},
|
|
822
694
|
});
|
|
823
695
|
|
|
824
|
-
assert.calledOnceWithExactly(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
696
|
+
assert.calledOnceWithExactly(spy, {
|
|
697
|
+
captionLanguages: [],
|
|
698
|
+
maxLanguages: 0,
|
|
699
|
+
spokenLanguages: [],
|
|
700
|
+
currentSpokenLanguage: 'en',
|
|
829
701
|
});
|
|
830
702
|
});
|
|
831
703
|
|
|
832
|
-
it('processes
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
recording_file_name:
|
|
847
|
-
'OkayWebEx_fd5bd0fc-06fb-4fd1-982b-554c4368f101_47900f3f-8579-25eb-3f6a-74d81a3c66a4_2335.8900000000003_2336.79.raw',
|
|
848
|
-
type: 'live-hotword',
|
|
704
|
+
it('processes transcription interim results', () => {
|
|
705
|
+
const spy = sinon.spy();
|
|
706
|
+
voiceaChannel.on(EVENT_TRIGGERS.NEW_CAPTION, spy);
|
|
707
|
+
|
|
708
|
+
eventHandler({
|
|
709
|
+
sequenceNumber: 1,
|
|
710
|
+
headers: {},
|
|
711
|
+
data: {
|
|
712
|
+
relayType: 'voicea.transcription',
|
|
713
|
+
voiceaPayload: {
|
|
714
|
+
type: 'transcript_interim_results',
|
|
715
|
+
transcript_id: 'tid-1',
|
|
716
|
+
transcripts: [{text: 'Hello'}],
|
|
717
|
+
},
|
|
849
718
|
},
|
|
850
|
-
ts: 1616137504.8107769,
|
|
851
|
-
type: 'eva_wake',
|
|
852
|
-
};
|
|
853
|
-
|
|
854
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
855
|
-
await voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
856
|
-
headers: {from: 'ws'},
|
|
857
|
-
data: {relayType: 'voicea.transcription', voiceaPayload},
|
|
858
719
|
});
|
|
859
720
|
|
|
860
|
-
assert.calledOnceWithExactly(
|
|
861
|
-
|
|
862
|
-
|
|
721
|
+
assert.calledOnceWithExactly(spy, {
|
|
722
|
+
isFinal: false,
|
|
723
|
+
transcriptId: 'tid-1',
|
|
724
|
+
transcripts: [{text: 'Hello'}],
|
|
863
725
|
});
|
|
864
726
|
});
|
|
865
727
|
|
|
866
|
-
it('processes
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const voiceaPayload = {
|
|
870
|
-
audio_received_millis: 0,
|
|
871
|
-
command_response: 'OK! Decision created.',
|
|
872
|
-
id: '9bc51440-1a22-7c81-6add-4b6ff7b59f7c',
|
|
873
|
-
intent: 'decision',
|
|
874
|
-
meeting: 'fd5bd0fc-06fb-4fd1-982b-554c4368f101',
|
|
875
|
-
ts: 1616135828.2552843,
|
|
876
|
-
type: 'eva_thanks',
|
|
877
|
-
};
|
|
728
|
+
it('processes transcription final results', () => {
|
|
729
|
+
const spy = sinon.spy();
|
|
730
|
+
voiceaChannel.on(EVENT_TRIGGERS.NEW_CAPTION, spy);
|
|
878
731
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
headers: {
|
|
882
|
-
data: {
|
|
732
|
+
eventHandler({
|
|
733
|
+
sequenceNumber: 1,
|
|
734
|
+
headers: {},
|
|
735
|
+
data: {
|
|
736
|
+
relayType: 'voicea.transcription',
|
|
737
|
+
voiceaPayload: {
|
|
738
|
+
type: 'transcript_final_result',
|
|
739
|
+
transcript_id: 'tid-2',
|
|
740
|
+
transcripts: [{text: 'Hello world', end_millis: 60000}],
|
|
741
|
+
},
|
|
742
|
+
},
|
|
883
743
|
});
|
|
884
744
|
|
|
885
|
-
assert.
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
});
|
|
745
|
+
assert.calledOnce(spy);
|
|
746
|
+
const call = spy.getCall(0);
|
|
747
|
+
assert.equal(call.args[0].isFinal, true);
|
|
748
|
+
assert.equal(call.args[0].transcriptId, 'tid-2');
|
|
890
749
|
});
|
|
891
750
|
|
|
892
|
-
it('processes
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const voiceaPayload = {
|
|
896
|
-
audio_received_millis: 0,
|
|
897
|
-
command_response: '',
|
|
898
|
-
id: '9bc51440-1a22-7c81-6add-4b6ff7b59f7c',
|
|
899
|
-
intent: 'decision',
|
|
900
|
-
meeting: 'fd5bd0fc-06fb-4fd1-982b-554c4368f101',
|
|
901
|
-
ts: 1616135828.2552843,
|
|
902
|
-
type: 'eva_cancel',
|
|
903
|
-
};
|
|
751
|
+
it('processes caption language response success', () => {
|
|
752
|
+
const spy = sinon.spy();
|
|
753
|
+
voiceaChannel.on(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, spy);
|
|
904
754
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
headers: {
|
|
908
|
-
data: {
|
|
755
|
+
eventHandler({
|
|
756
|
+
sequenceNumber: 1,
|
|
757
|
+
headers: {},
|
|
758
|
+
data: {
|
|
759
|
+
relayType: 'voicea.transl.rsp',
|
|
760
|
+
voiceaPayload: {statusCode: 200},
|
|
761
|
+
},
|
|
909
762
|
});
|
|
910
763
|
|
|
911
|
-
assert.calledOnceWithExactly(
|
|
764
|
+
assert.calledOnceWithExactly(spy, {statusCode: 200});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('processes caption language response error', () => {
|
|
768
|
+
const spy = sinon.spy();
|
|
769
|
+
voiceaChannel.on(EVENT_TRIGGERS.CAPTION_LANGUAGE_UPDATE, spy);
|
|
912
770
|
|
|
913
|
-
|
|
914
|
-
|
|
771
|
+
eventHandler({
|
|
772
|
+
sequenceNumber: 1,
|
|
773
|
+
headers: {},
|
|
774
|
+
data: {
|
|
775
|
+
relayType: 'voicea.transl.rsp',
|
|
776
|
+
voiceaPayload: {statusCode: 400, errorCode: 400, message: 'Bad request'},
|
|
777
|
+
},
|
|
915
778
|
});
|
|
779
|
+
|
|
780
|
+
assert.calledOnceWithExactly(spy, {statusCode: 400, errorMessage: 'Bad request'});
|
|
916
781
|
});
|
|
917
782
|
|
|
918
|
-
it('processes
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
trigger_info: {type: 'live-hotword'},
|
|
783
|
+
it('processes manual transcription events', () => {
|
|
784
|
+
const spy = sinon.spy();
|
|
785
|
+
voiceaChannel.on(EVENT_TRIGGERS.NEW_MANUAL_CAPTION, spy);
|
|
786
|
+
|
|
787
|
+
eventHandler({
|
|
788
|
+
sequenceNumber: 1,
|
|
789
|
+
headers: {from: 'user-1'},
|
|
790
|
+
data: {
|
|
791
|
+
relayType: 'client.manual_transcription',
|
|
792
|
+
transcriptPayload: {
|
|
793
|
+
type: 'manual_caption_final_result',
|
|
794
|
+
id: 'manual-id',
|
|
795
|
+
transcripts: [{text: 'Manual caption'}],
|
|
796
|
+
},
|
|
933
797
|
},
|
|
934
|
-
id: 'e6df0262-6289-db2e-581a-d44bb41b1c9c',
|
|
935
|
-
meeting: 'fd5bd0fc-06fb-4fd1-982b-554c4368f101',
|
|
936
|
-
ts: 1616135858.5349569,
|
|
937
|
-
type: 'highlight_created',
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
941
|
-
await voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
942
|
-
headers: {from: 'ws'},
|
|
943
|
-
data: {relayType: 'voicea.transcription', voiceaPayload},
|
|
944
798
|
});
|
|
945
799
|
|
|
946
|
-
assert.
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
text: 'Create a decision to move ahead with the last proposal.',
|
|
951
|
-
highlightLabel: 'Decision',
|
|
952
|
-
highlightSource: 'voice-command',
|
|
953
|
-
timestamp: '11:00',
|
|
954
|
-
});
|
|
800
|
+
assert.calledOnce(spy);
|
|
801
|
+
const call = spy.getCall(0);
|
|
802
|
+
assert.equal(call.args[0].isFinal, true);
|
|
803
|
+
assert.equal(call.args[0].transcriptId, 'manual-id');
|
|
955
804
|
});
|
|
956
805
|
|
|
957
|
-
it('processes
|
|
958
|
-
|
|
806
|
+
it('processes highlight created events', () => {
|
|
807
|
+
const spy = sinon.spy();
|
|
808
|
+
voiceaChannel.on(EVENT_TRIGGERS.HIGHLIGHT_CREATED, spy);
|
|
959
809
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
810
|
+
eventHandler({
|
|
811
|
+
sequenceNumber: 1,
|
|
812
|
+
headers: {},
|
|
813
|
+
data: {
|
|
814
|
+
relayType: 'voicea.transcription',
|
|
815
|
+
voiceaPayload: {
|
|
816
|
+
type: 'highlight_created',
|
|
817
|
+
highlight: {
|
|
818
|
+
csis: [123],
|
|
819
|
+
highlight_id: 'h-1',
|
|
820
|
+
transcript: 'Highlighted text',
|
|
821
|
+
highlight_label: 'important',
|
|
822
|
+
highlight_source: 'user',
|
|
823
|
+
end_millis: 30000,
|
|
824
|
+
},
|
|
825
|
+
},
|
|
971
826
|
},
|
|
827
|
+
});
|
|
972
828
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
const spy = sinon.spy();
|
|
829
|
+
assert.calledOnce(spy);
|
|
830
|
+
});
|
|
977
831
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
832
|
+
it('processes eva wake events', () => {
|
|
833
|
+
const spy = sinon.spy();
|
|
834
|
+
voiceaChannel.on(EVENT_TRIGGERS.EVA_COMMAND, spy);
|
|
981
835
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
headers: {
|
|
985
|
-
data: {
|
|
836
|
+
eventHandler({
|
|
837
|
+
sequenceNumber: 1,
|
|
838
|
+
headers: {},
|
|
839
|
+
data: {
|
|
840
|
+
relayType: 'voicea.transcription',
|
|
841
|
+
voiceaPayload: {type: 'eva_wake'},
|
|
842
|
+
},
|
|
986
843
|
});
|
|
987
844
|
|
|
988
|
-
assert.calledOnceWithExactly(
|
|
989
|
-
assert.calledOnceWithExactly(triggerSpy, {
|
|
990
|
-
languageCode: 'en',
|
|
991
|
-
});
|
|
845
|
+
assert.calledOnceWithExactly(spy, {isListening: true});
|
|
992
846
|
});
|
|
993
847
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
let triggerSpy, functionSpy;
|
|
848
|
+
it('processes eva cancel events', () => {
|
|
849
|
+
const spy = sinon.spy();
|
|
850
|
+
voiceaChannel.on(EVENT_TRIGGERS.EVA_COMMAND, spy);
|
|
998
851
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
voiceaService.on(EVENT_TRIGGERS.NEW_MANUAL_CAPTION, triggerSpy);
|
|
1007
|
-
|
|
1008
|
-
const transcriptPayload = {
|
|
1009
|
-
id: "747d711d-3414-fd69-7081-e842649f2d28",
|
|
1010
|
-
transcripts: [
|
|
1011
|
-
{
|
|
1012
|
-
text: "Good",
|
|
1013
|
-
}
|
|
1014
|
-
],
|
|
1015
|
-
type: "manual_caption_interim_result",
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
1019
|
-
await voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
1020
|
-
headers: {from: 'ws'},
|
|
1021
|
-
data: {relayType: 'aibridge.manual_transcription', transcriptPayload},
|
|
852
|
+
eventHandler({
|
|
853
|
+
sequenceNumber: 1,
|
|
854
|
+
headers: {},
|
|
855
|
+
data: {
|
|
856
|
+
relayType: 'voicea.transcription',
|
|
857
|
+
voiceaPayload: {type: 'eva_cancel'},
|
|
858
|
+
},
|
|
1022
859
|
});
|
|
1023
860
|
|
|
1024
|
-
assert.calledOnceWithExactly(
|
|
1025
|
-
assert.calledOnceWithExactly(triggerSpy, {
|
|
1026
|
-
isFinal: false,
|
|
1027
|
-
transcriptId: '747d711d-3414-fd69-7081-e842649f2d28',
|
|
1028
|
-
transcripts: transcriptPayload.transcripts,
|
|
1029
|
-
sender: 'ws',
|
|
1030
|
-
source: 'aibridge.manual_transcription'
|
|
1031
|
-
});
|
|
861
|
+
assert.calledOnceWithExactly(spy, {isListening: false});
|
|
1032
862
|
});
|
|
1033
863
|
|
|
1034
|
-
it('processes
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const transcriptPayload = {
|
|
1038
|
-
id: "8d226d31-044a-8d11-cc39-cedbde183154",
|
|
1039
|
-
transcripts: [
|
|
1040
|
-
{
|
|
1041
|
-
text: "Good Morning",
|
|
1042
|
-
start_millis: 10420,
|
|
1043
|
-
end_millis: 11380,
|
|
1044
|
-
}
|
|
1045
|
-
],
|
|
1046
|
-
type: "manual_caption_final_result",
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
1050
|
-
await voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
1051
|
-
headers: {from: 'ws'},
|
|
1052
|
-
data: {relayType: 'aibridge.manual_transcription', transcriptPayload},
|
|
1053
|
-
});
|
|
864
|
+
it('processes eva thanks events', () => {
|
|
865
|
+
const spy = sinon.spy();
|
|
866
|
+
voiceaChannel.on(EVENT_TRIGGERS.EVA_COMMAND, spy);
|
|
1054
867
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
868
|
+
eventHandler({
|
|
869
|
+
sequenceNumber: 1,
|
|
870
|
+
headers: {},
|
|
871
|
+
data: {
|
|
872
|
+
relayType: 'voicea.transcription',
|
|
873
|
+
voiceaPayload: {type: 'eva_thanks', command_response: 'OK, noted'},
|
|
874
|
+
},
|
|
1062
875
|
});
|
|
1063
|
-
});
|
|
1064
876
|
|
|
1065
|
-
|
|
1066
|
-
|
|
877
|
+
assert.calledOnceWithExactly(spy, {isListening: false, text: 'OK, noted'});
|
|
878
|
+
});
|
|
1067
879
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
{
|
|
1072
|
-
text: "Good",
|
|
1073
|
-
}
|
|
1074
|
-
],
|
|
1075
|
-
type: "manual_caption_interim_result",
|
|
1076
|
-
};
|
|
880
|
+
it('processes language detected when in spoken languages', () => {
|
|
881
|
+
const spy = sinon.spy();
|
|
882
|
+
voiceaChannel.on(EVENT_TRIGGERS.LANGUAGE_DETECTED, spy);
|
|
1077
883
|
|
|
1078
|
-
//
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
884
|
+
// First set up spoken languages via announcement
|
|
885
|
+
eventHandler({
|
|
886
|
+
sequenceNumber: 1,
|
|
887
|
+
headers: {from: 'ws-service'},
|
|
888
|
+
data: {
|
|
889
|
+
relayType: 'voicea.annc',
|
|
890
|
+
voiceaPayload: {
|
|
891
|
+
ASR: {spoken_languages: ['en', 'es', 'fr']},
|
|
892
|
+
},
|
|
893
|
+
},
|
|
1082
894
|
});
|
|
1083
895
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
896
|
+
// Then emit language detected
|
|
897
|
+
eventHandler({
|
|
898
|
+
sequenceNumber: 2,
|
|
899
|
+
headers: {},
|
|
900
|
+
data: {
|
|
901
|
+
relayType: 'voicea.transcription',
|
|
902
|
+
voiceaPayload: {type: 'language_detected', language: 'es'},
|
|
903
|
+
},
|
|
1091
904
|
});
|
|
905
|
+
|
|
906
|
+
assert.calledOnceWithExactly(spy, {languageCode: 'es'});
|
|
1092
907
|
});
|
|
1093
908
|
|
|
1094
|
-
it('
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
1110
|
-
await voiceaService.webex.internal.llm._emit('event:relay.event', {
|
|
1111
|
-
headers: {from: '654321'},
|
|
1112
|
-
data: {relayType: 'client.manual_transcription', transcriptPayload},
|
|
909
|
+
it('does not emit language detected when not in spoken languages', () => {
|
|
910
|
+
const spy = sinon.spy();
|
|
911
|
+
voiceaChannel.on(EVENT_TRIGGERS.LANGUAGE_DETECTED, spy);
|
|
912
|
+
|
|
913
|
+
// First set up spoken languages via announcement
|
|
914
|
+
eventHandler({
|
|
915
|
+
sequenceNumber: 1,
|
|
916
|
+
headers: {from: 'ws-service'},
|
|
917
|
+
data: {
|
|
918
|
+
relayType: 'voicea.annc',
|
|
919
|
+
voiceaPayload: {
|
|
920
|
+
ASR: {spoken_languages: ['en', 'es']},
|
|
921
|
+
},
|
|
922
|
+
},
|
|
1113
923
|
});
|
|
1114
924
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
925
|
+
// Then emit language detected for unsupported language
|
|
926
|
+
eventHandler({
|
|
927
|
+
sequenceNumber: 2,
|
|
928
|
+
headers: {},
|
|
929
|
+
data: {
|
|
930
|
+
relayType: 'voicea.transcription',
|
|
931
|
+
voiceaPayload: {type: 'language_detected', language: 'de'},
|
|
932
|
+
},
|
|
1122
933
|
});
|
|
1123
|
-
});
|
|
1124
|
-
});
|
|
1125
934
|
|
|
1126
|
-
|
|
1127
|
-
it('works correctly', () => {
|
|
1128
|
-
voiceaService.captionStatus = "enabled"
|
|
1129
|
-
assert.equal(voiceaService.getCaptionStatus(), "enabled");
|
|
935
|
+
assert.notCalled(spy);
|
|
1130
936
|
});
|
|
1131
937
|
});
|
|
1132
938
|
|
|
1133
|
-
describe(
|
|
1134
|
-
it('
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
});
|
|
939
|
+
describe('#switchLLMChannel', () => {
|
|
940
|
+
it('switches to a new LLM channel and preserves caption state', async () => {
|
|
941
|
+
// First enable captions
|
|
942
|
+
voiceaChannel.keepTranscriptionSubscribed = true;
|
|
943
|
+
voiceaChannel.currentSpokenLanguage = 'es';
|
|
1139
944
|
|
|
1140
|
-
|
|
1141
|
-
it('should trigger SPOKEN_LANGUAGE_UPDATE event with correct languageCode', () => {
|
|
1142
|
-
const triggerSpy = sinon.spy();
|
|
1143
|
-
voiceaService.on(EVENT_TRIGGERS.SPOKEN_LANGUAGE_UPDATE, triggerSpy);
|
|
945
|
+
const newMockLLMChannel = createMockLLMChannel({locusUrl: 'newLocusUrl'});
|
|
1144
946
|
|
|
1145
|
-
|
|
1146
|
-
voiceaService.onSpokenLanguageUpdate(languageCode, '123');
|
|
1147
|
-
assert.equal(voiceaService.currentSpokenLanguage, languageCode);
|
|
1148
|
-
assert.calledOnceWithExactly(triggerSpy, {languageCode, meetingId: '123'});
|
|
1149
|
-
});
|
|
1150
|
-
});
|
|
947
|
+
await voiceaChannel.switchLLMChannel(newMockLLMChannel);
|
|
1151
948
|
|
|
1152
|
-
|
|
1153
|
-
|
|
949
|
+
// Should have unsubscribed from old channel
|
|
950
|
+
assert.calledWith(mockLLMChannel.off, 'event:relay.event', sinon.match.func);
|
|
1154
951
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
voiceaService.webex.internal.llm.socket = mockWebSocket;
|
|
1158
|
-
voiceaService.webex.internal.llm.isConnected.returns(true);
|
|
1159
|
-
voiceaService.seqNum = 1;
|
|
1160
|
-
});
|
|
952
|
+
// Should have subscribed to new channel
|
|
953
|
+
assert.calledWith(newMockLLMChannel.on, 'event:relay.event', sinon.match.func);
|
|
1161
954
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
voiceaService.currentCaptionLanguage = 'en';
|
|
955
|
+
// Should have reset announcement state to joining (since captions were on and it re-announced)
|
|
956
|
+
assert.equal(voiceaChannel.getAnnounceStatus(), 'joining');
|
|
1165
957
|
|
|
1166
|
-
|
|
1167
|
-
|
|
958
|
+
// turnOnCaptions sends both an announcement and a subchannel subscription
|
|
959
|
+
assert.calledTwice(newMockLLMChannel.socket.send);
|
|
1168
960
|
|
|
1169
|
-
|
|
1170
|
-
assert.
|
|
961
|
+
// First call should be the announcement
|
|
962
|
+
assert.calledWithExactly(newMockLLMChannel.socket.send.getCall(0), {
|
|
963
|
+
id: '1',
|
|
964
|
+
type: 'publishRequest',
|
|
965
|
+
recipients: [{route: 'binding'}],
|
|
966
|
+
headers: {},
|
|
967
|
+
data: {
|
|
968
|
+
clientPayload: {
|
|
969
|
+
version: 'v2',
|
|
970
|
+
},
|
|
971
|
+
eventType: 'relay.event',
|
|
972
|
+
relayType: 'client.annc',
|
|
973
|
+
},
|
|
974
|
+
trackingId: sinon.match.string,
|
|
975
|
+
});
|
|
1171
976
|
});
|
|
1172
977
|
|
|
1173
|
-
it('
|
|
1174
|
-
|
|
1175
|
-
|
|
978
|
+
it('does not turn on captions if they were not on before switching', async () => {
|
|
979
|
+
// Captions are off
|
|
980
|
+
voiceaChannel.keepTranscriptionSubscribed = false;
|
|
1176
981
|
|
|
1177
|
-
|
|
982
|
+
const newMockLLMChannel = createMockLLMChannel({locusUrl: 'newLocusUrl'});
|
|
1178
983
|
|
|
1179
|
-
|
|
1180
|
-
assert.notCalled(voiceaService.webex.internal.llm.socket.send);
|
|
1181
|
-
});
|
|
984
|
+
await voiceaChannel.switchLLMChannel(newMockLLMChannel);
|
|
1182
985
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
voiceaService.currentCaptionLanguage = 'es';
|
|
986
|
+
// Should have unsubscribed from old channel
|
|
987
|
+
assert.calledWith(mockLLMChannel.off, 'event:relay.event', sinon.match.func);
|
|
1186
988
|
|
|
1187
|
-
|
|
989
|
+
// Should have subscribed to new channel
|
|
990
|
+
assert.calledWith(newMockLLMChannel.on, 'event:relay.event', sinon.match.func);
|
|
1188
991
|
|
|
1189
|
-
|
|
1190
|
-
assert.
|
|
992
|
+
// Should NOT have sent any messages (no announcement for captions)
|
|
993
|
+
assert.notCalled(newMockLLMChannel.socket.send);
|
|
1191
994
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
expect(callArgs).to.have.nested.property('data.clientPayload.translationLanguage', 'es');
|
|
995
|
+
// State should be idle since captions weren't re-enabled
|
|
996
|
+
assert.equal(voiceaChannel.getAnnounceStatus(), 'idle');
|
|
1195
997
|
});
|
|
1196
998
|
|
|
1197
|
-
it('
|
|
1198
|
-
|
|
1199
|
-
|
|
999
|
+
it('handles case when not previously subscribed to events', async () => {
|
|
1000
|
+
// Create a new channel that hasn't subscribed to events
|
|
1001
|
+
const freshVoiceaChannel = new VoiceaChannel(mockLLMChannel, requestStub);
|
|
1002
|
+
freshVoiceaChannel.hasSubscribedToEvents = false;
|
|
1003
|
+
|
|
1004
|
+
const newMockLLMChannel = createMockLLMChannel({locusUrl: 'newLocusUrl'});
|
|
1200
1005
|
|
|
1201
|
-
|
|
1006
|
+
await freshVoiceaChannel.switchLLMChannel(newMockLLMChannel);
|
|
1202
1007
|
|
|
1203
|
-
|
|
1204
|
-
assert.
|
|
1008
|
+
// Should NOT have tried to unsubscribe from old channel (wasn't subscribed)
|
|
1009
|
+
assert.neverCalledWith(mockLLMChannel.off, 'event:relay.event', sinon.match.func);
|
|
1010
|
+
|
|
1011
|
+
// Should have subscribed to new channel
|
|
1012
|
+
assert.calledWith(newMockLLMChannel.on, 'event:relay.event', sinon.match.func);
|
|
1205
1013
|
});
|
|
1206
1014
|
});
|
|
1207
|
-
|
|
1208
1015
|
});
|
|
1209
1016
|
});
|