@webex/internal-plugin-voicea 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.11
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/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/voicea-plugin.d.ts +3 -3
- package/dist/types/voicea.d.ts +51 -7
- package/dist/types/voicea.types.d.ts +18 -1
- package/dist/voicea-plugin.js +2 -2
- package/dist/voicea-plugin.js.map +1 -1
- package/dist/voicea.js +169 -70
- package/dist/voicea.js.map +1 -1
- package/dist/voicea.types.js.map +1 -1
- package/package.json +8 -8
- package/src/constants.ts +2 -0
- package/src/index.ts +2 -2
- package/src/voicea-plugin.ts +3 -3
- package/src/voicea.ts +156 -50
- package/src/voicea.types.ts +21 -0
- package/test/unit/spec/voicea-plugin.js +12 -0
- package/test/unit/spec/voicea.js +282 -4
package/dist/voicea.types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["voicea.types.ts"],"sourcesContent":["/* eslint-disable camelcase */\n/* eslint-disable no-undef */\n\n/**\n * Type for payload\n */\ninterface AnnouncementPayload {\n translation: {\n max_languages: number;\n allowed_languages: string[];\n };\n\n ASR: {\n spoken_languages: string[];\n };\n}\n\n// eslint-disable-next-line no-undef\ntype TRANSCRIPTION_TYPES =\n | 'unknown'\n | 'eva_wake'\n | 'eva_thanks'\n | 'eva_cancel'\n | 'highlight_created'\n | 'transcript_interim_results'\n | 'transcript_final_result'\n | 'manual_caption_interim_results'\n | 'manual_caption_interim_result'\n | 'manual_caption_final_result';\n\n/**\n * Class for an Transcription Object\n */\ninterface Transcription {\n start_millis: number;\n end_millis: number;\n text: string;\n transcript_language_code: string;\n translations: {[x: string]: string};\n csis: number[];\n last_packet_timestamp_ms: number;\n timestamp: string;\n}\n\n/**\n * Highlights\n */\ninterface Highlight {\n highlight_id: string;\n transcript: string;\n highlight_label: string;\n highlight_source: string;\n start_millis: number;\n end_millis: number;\n csis: number[];\n}\n/**\n * Type for Transcription message\n */\ninterface TranscriptionResponse {\n type: TRANSCRIPTION_TYPES;\n id: string;\n sender: string;\n data_source: string;\n transcript_id: string;\n translations?: {[x: string]: string};\n transcripts?: Transcription[];\n transcript?: Transcription;\n highlight?: Highlight;\n csis: number[];\n data: string;\n command_response: string;\n language: string;\n}\n/**\n * Type for CaptionLanguageResponse\n */\ninterface CaptionLanguageResponse {\n requestId: string;\n statusCode: number;\n errorCode: number;\n message: string;\n}\n\ninterface IVoiceaChannel {\n setSpokenLanguage: (languageCode: string) => Promise<void>;\n onSpokenLanguageUpdate: (languageCode: string, meetingId: string) => void;\n requestLanguage: (languageCode: string) => void;\n turnOnCaptions: () => undefined | Promise<void>;\n toggleTranscribing: (activate: boolean, spokenLanguage: string) => undefined | Promise<void>;\n deregisterEvents: () => void;\n toggleManualCaption: (enable: boolean) => undefined | Promise<void>;\n sendManualClosedCaption: (\n text: string,\n timeStamp: number,\n csis: number[],\n isFinal: boolean\n ) => void;\n getKeepTranscriptionSubscribed: () => boolean;\n}\n\ntype MeetingTranscripts = {\n start_mills?: number;\n end_mills?: number;\n text: string;\n last_packet_timestamp_ms?: number;\n csis: Array<number>;\n transcript_language_code: string;\n translations?: {\n [key: string]: string;\n };\n timestamp?: string;\n};\n\ntype MeetingTranscriptPayload = {\n isFinal: boolean;\n transcriptId: string;\n transcripts: Array<MeetingTranscripts>;\n};\n\nexport type {\n AnnouncementPayload,\n CaptionLanguageResponse,\n TranscriptionResponse,\n Transcription,\n Highlight,\n IVoiceaChannel,\n MeetingTranscriptPayload,\n};\n"],"mappings":"","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["voicea.types.ts"],"sourcesContent":["/* eslint-disable camelcase */\n/* eslint-disable no-undef */\n\n/**\n * Type for payload\n */\ninterface AnnouncementPayload {\n translation: {\n max_languages: number;\n allowed_languages: string[];\n };\n\n ASR: {\n spoken_languages: string[];\n };\n}\n\n// eslint-disable-next-line no-undef\ntype TRANSCRIPTION_TYPES =\n | 'unknown'\n | 'eva_wake'\n | 'eva_thanks'\n | 'eva_cancel'\n | 'highlight_created'\n | 'transcript_interim_results'\n | 'transcript_final_result'\n | 'manual_caption_interim_results'\n | 'manual_caption_interim_result'\n | 'manual_caption_final_result';\n\n/**\n * Class for an Transcription Object\n */\ninterface Transcription {\n start_millis: number;\n end_millis: number;\n text: string;\n transcript_language_code: string;\n translations: {[x: string]: string};\n csis: number[];\n last_packet_timestamp_ms: number;\n timestamp: string;\n taggedSpeaker?: {\n speakerId?: string[];\n newName?: string;\n };\n}\n\ninterface TaggedSpeaker {\n csiId: number;\n speakerId: string;\n newName: string;\n}\n\ninterface SpeakerNameUpdatePayload {\n id?: string;\n taggedSpeakers: TaggedSpeaker[];\n}\n\n/**\n * Highlights\n */\ninterface Highlight {\n highlight_id: string;\n transcript: string;\n highlight_label: string;\n highlight_source: string;\n start_millis: number;\n end_millis: number;\n csis: number[];\n}\n/**\n * Type for Transcription message\n */\ninterface TranscriptionResponse {\n type: TRANSCRIPTION_TYPES;\n id: string;\n sender: string;\n data_source: string;\n transcript_id: string;\n translations?: {[x: string]: string};\n transcripts?: Transcription[];\n transcript?: Transcription;\n highlight?: Highlight;\n csis: number[];\n data: string;\n command_response: string;\n language: string;\n}\n/**\n * Type for CaptionLanguageResponse\n */\ninterface CaptionLanguageResponse {\n requestId: string;\n statusCode: number;\n errorCode: number;\n message: string;\n}\n\ninterface IVoiceaChannel {\n setSpokenLanguage: (languageCode: string) => Promise<void>;\n onSpokenLanguageUpdate: (languageCode: string, meetingId: string) => void;\n requestLanguage: (languageCode: string) => void;\n turnOnCaptions: () => undefined | Promise<void>;\n toggleTranscribing: (activate: boolean, spokenLanguage: string) => undefined | Promise<void>;\n deregisterEvents: () => void;\n toggleManualCaption: (enable: boolean) => undefined | Promise<void>;\n sendManualClosedCaption: (\n text: string,\n timeStamp: number,\n csis: number[],\n isFinal: boolean\n ) => void;\n getKeepTranscriptionSubscribed: () => boolean;\n}\n\ntype MeetingTranscripts = {\n start_mills?: number;\n end_mills?: number;\n text: string;\n last_packet_timestamp_ms?: number;\n csis: Array<number>;\n transcript_language_code: string;\n translations?: {\n [key: string]: string;\n };\n timestamp?: string;\n taggedSpeaker?: {\n speakerId?: string[];\n newName?: string;\n };\n};\n\ntype MeetingTranscriptPayload = {\n isFinal: boolean;\n transcriptId: string;\n transcripts: Array<MeetingTranscripts>;\n};\n\nexport type {\n AnnouncementPayload,\n CaptionLanguageResponse,\n TranscriptionResponse,\n Transcription,\n Highlight,\n TaggedSpeaker,\n SpeakerNameUpdatePayload,\n IVoiceaChannel,\n MeetingTranscriptPayload,\n};\n"],"mappings":"","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"node": ">=18"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@webex/internal-plugin-llm": "3.12.0-llmrefactor.
|
|
18
|
-
"@webex/internal-plugin-mercury": "3.12.0-llmrefactor.
|
|
19
|
-
"@webex/webex-core": "3.12.0-llmrefactor.
|
|
17
|
+
"@webex/internal-plugin-llm": "3.12.0-llmrefactor.9",
|
|
18
|
+
"@webex/internal-plugin-mercury": "3.12.0-llmrefactor.9",
|
|
19
|
+
"@webex/webex-core": "3.12.0-llmrefactor.7",
|
|
20
20
|
"uuid": "^3.3.2"
|
|
21
21
|
},
|
|
22
22
|
"browserify": {
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"@webex/eslint-config-legacy": "0.0.0",
|
|
32
32
|
"@webex/jest-config-legacy": "0.0.0",
|
|
33
33
|
"@webex/legacy-tools": "0.0.0",
|
|
34
|
-
"@webex/test-helper-chai": "3.12.0-llmrefactor.
|
|
35
|
-
"@webex/test-helper-mocha": "3.12.0-llmrefactor.
|
|
36
|
-
"@webex/test-helper-mock-web-socket": "3.12.0-llmrefactor.
|
|
34
|
+
"@webex/test-helper-chai": "3.12.0-llmrefactor.2",
|
|
35
|
+
"@webex/test-helper-mocha": "3.12.0-llmrefactor.2",
|
|
36
|
+
"@webex/test-helper-mock-web-socket": "3.12.0-llmrefactor.2",
|
|
37
37
|
"@webex/test-helper-mock-webex": "3.12.0-llmrefactor.1",
|
|
38
|
-
"@webex/test-helper-test-users": "3.12.0-llmrefactor.
|
|
38
|
+
"@webex/test-helper-test-users": "3.12.0-llmrefactor.0",
|
|
39
39
|
"eslint": "^8.24.0",
|
|
40
40
|
"prettier": "^2.7.1",
|
|
41
41
|
"sinon": "^9.2.4"
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"test:style": "eslint ./src/**/*.*",
|
|
50
50
|
"test:unit": "webex-legacy-tools test --unit --runner jest"
|
|
51
51
|
},
|
|
52
|
-
"version": "3.12.0-llmrefactor.
|
|
52
|
+
"version": "3.12.0-llmrefactor.11"
|
|
53
53
|
}
|
package/src/constants.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const EVENT_TRIGGERS = {
|
|
|
7
7
|
TRANSCRIBING_OFF: 'voicea:transcribingOff',
|
|
8
8
|
|
|
9
9
|
NEW_CAPTION: 'voicea:newCaption',
|
|
10
|
+
SPEAKER_NAME_UPDATED: 'voicea:speakerNameUpdated',
|
|
10
11
|
EVA_COMMAND: 'voicea:wxa',
|
|
11
12
|
HIGHLIGHT_CREATED: 'voicea:highlightCreated',
|
|
12
13
|
NEW_MANUAL_CAPTION: 'aibridge:newManualCaption',
|
|
@@ -21,6 +22,7 @@ export const AIBRIDGE_RELAY_TYPES = {
|
|
|
21
22
|
TRANSLATION_REQUEST: 'voicea.transl.req',
|
|
22
23
|
TRANSLATION_RESPONSE: 'voicea.transl.rsp',
|
|
23
24
|
TRANSCRIPTION: 'voicea.transcription',
|
|
25
|
+
SPEAKER_NAME_UPDATE: 'voicea.update_speakername',
|
|
24
26
|
},
|
|
25
27
|
MANUAL: {
|
|
26
28
|
TRANSCRIPTION: 'aibridge.manual_transcription',
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,11 @@ import * as WebexCore from '@webex/webex-core';
|
|
|
2
2
|
|
|
3
3
|
import Voicea from './voicea-plugin';
|
|
4
4
|
import {VoiceaChannel} from './voicea';
|
|
5
|
-
import type {MeetingTranscriptPayload} from './voicea.types';
|
|
5
|
+
import type {MeetingTranscriptPayload, SpeakerNameUpdatePayload} from './voicea.types';
|
|
6
6
|
|
|
7
7
|
WebexCore.registerInternalPlugin('voicea', Voicea, {});
|
|
8
8
|
|
|
9
9
|
export {Voicea as VoiceaPlugin, VoiceaChannel};
|
|
10
10
|
export default Voicea;
|
|
11
|
-
export {type MeetingTranscriptPayload};
|
|
11
|
+
export {type MeetingTranscriptPayload, type SpeakerNameUpdatePayload};
|
|
12
12
|
export {EVENT_TRIGGERS, TURN_ON_CAPTION_STATUS} from './constants';
|
package/src/voicea-plugin.ts
CHANGED
|
@@ -13,16 +13,16 @@ export class VoiceaPlugin extends WebexPlugin {
|
|
|
13
13
|
namespace = VOICEA;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Creates a VoiceaChannel bound to
|
|
16
|
+
* Creates a VoiceaChannel, optionally bound to an LLMChannel.
|
|
17
17
|
*
|
|
18
18
|
* Note: VoiceaChannel does not take ownership of the LLMChannel.
|
|
19
19
|
* The caller retains ownership and is responsible for the LLMChannel's
|
|
20
20
|
* lifecycle (connection, disconnection, cleanup).
|
|
21
21
|
*
|
|
22
|
-
* @param {LLMChannel} llmChannel - The LLM channel to use for voicea
|
|
22
|
+
* @param {LLMChannel} [llmChannel] - The LLM channel to use for voicea (optional)
|
|
23
23
|
* @returns {VoiceaChannel} A new VoiceaChannel instance
|
|
24
24
|
*/
|
|
25
|
-
public createChannel(llmChannel
|
|
25
|
+
public createChannel(llmChannel?: LLMChannel): VoiceaChannel {
|
|
26
26
|
// @ts-ignore - webex is available on WebexPlugin
|
|
27
27
|
const channel = new VoiceaChannel(llmChannel, this.webex.request.bind(this.webex));
|
|
28
28
|
|
package/src/voicea.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
AnnouncementPayload,
|
|
19
19
|
CaptionLanguageResponse,
|
|
20
|
+
SpeakerNameUpdatePayload,
|
|
20
21
|
TranscriptionResponse,
|
|
21
22
|
IVoiceaChannel,
|
|
22
23
|
} from './voicea.types';
|
|
@@ -31,7 +32,7 @@ import {millisToMinutesAndSeconds} from './utils';
|
|
|
31
32
|
*/
|
|
32
33
|
export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
33
34
|
private request: (options: {method: string; url: string; body?: object}) => Promise<any>;
|
|
34
|
-
private llmChannel
|
|
35
|
+
private llmChannel?: LLMChannel;
|
|
35
36
|
|
|
36
37
|
private seqNum: number;
|
|
37
38
|
private areCaptionsEnabled: boolean;
|
|
@@ -47,13 +48,21 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
47
48
|
private spokenLanguages: string[] = [];
|
|
48
49
|
private currentCaptionLanguage?: string;
|
|
49
50
|
|
|
51
|
+
// Target channel for reconciler pattern - the channel voicea WANTS to be bound to
|
|
52
|
+
private targetLLMChannel?: LLMChannel;
|
|
53
|
+
// Single pending 'online' listener for deferred reconciliation
|
|
54
|
+
private _pendingOnlineListener?: () => void;
|
|
55
|
+
// Tracks whether caption restoration is pending (set on switch, cleared on restore)
|
|
56
|
+
private _pendingCaptionRestore = false;
|
|
57
|
+
|
|
50
58
|
/**
|
|
51
|
-
* Creates a VoiceaChannel bound to
|
|
52
|
-
*
|
|
59
|
+
* Creates a VoiceaChannel, optionally bound to an LLMChannel.
|
|
60
|
+
* If no llmChannel is provided, call switchLLMChannel() later to attach one.
|
|
61
|
+
* @param {LLMChannel} [llmChannel] - The LLM channel to use (optional)
|
|
53
62
|
* @param {Function} request - The request function for making API calls (typically webex.request bound to webex)
|
|
54
63
|
*/
|
|
55
64
|
constructor(
|
|
56
|
-
llmChannel: LLMChannel,
|
|
65
|
+
llmChannel: LLMChannel | undefined,
|
|
57
66
|
request: (options: {method: string; url: string; body?: object}) => Promise<any>
|
|
58
67
|
) {
|
|
59
68
|
super();
|
|
@@ -70,9 +79,25 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
70
79
|
this.currentCaptionLanguage = undefined;
|
|
71
80
|
this.keepTranscriptionSubscribed = false;
|
|
72
81
|
|
|
73
|
-
// Subscribe to relay events from the LLM channel
|
|
74
|
-
this.llmChannel
|
|
75
|
-
|
|
82
|
+
// Subscribe to relay events from the LLM channel if provided
|
|
83
|
+
if (this.llmChannel) {
|
|
84
|
+
this.llmChannel.on('event:relay.event', this.eventProcessor);
|
|
85
|
+
this.hasSubscribedToEvents = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns the LLM channel, throwing if not available.
|
|
91
|
+
* @private
|
|
92
|
+
* @returns {LLMChannel}
|
|
93
|
+
* @throws {Error} If LLM channel is not available
|
|
94
|
+
*/
|
|
95
|
+
private requireLLMChannel(): LLMChannel {
|
|
96
|
+
if (!this.llmChannel) {
|
|
97
|
+
throw new Error('VoiceaChannel: LLM channel not available');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this.llmChannel;
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
/**
|
|
@@ -94,6 +119,9 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
94
119
|
case AIBRIDGE_RELAY_TYPES.VOICEA.TRANSCRIPTION:
|
|
95
120
|
this.processTranscription(e.data.voiceaPayload);
|
|
96
121
|
break;
|
|
122
|
+
case AIBRIDGE_RELAY_TYPES.VOICEA.SPEAKER_NAME_UPDATE:
|
|
123
|
+
this.processSpeakerNameUpdate(e.data.voiceaPayload);
|
|
124
|
+
break;
|
|
97
125
|
case AIBRIDGE_RELAY_TYPES.MANUAL.TRANSCRIPTION:
|
|
98
126
|
case AIBRIDGE_RELAY_TYPES.MANUAL.CAPTIONER:
|
|
99
127
|
this.processManualTranscription({
|
|
@@ -115,9 +143,14 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
115
143
|
this.areCaptionsEnabled = false;
|
|
116
144
|
this.keepTranscriptionSubscribed = false;
|
|
117
145
|
this.captionServiceId = undefined;
|
|
146
|
+
this.targetLLMChannel = undefined;
|
|
147
|
+
this._pendingCaptionRestore = false;
|
|
148
|
+
|
|
149
|
+
// Remove any pending online listener
|
|
150
|
+
this.detachPendingOnline();
|
|
118
151
|
|
|
119
152
|
if (this.hasSubscribedToEvents) {
|
|
120
|
-
this.llmChannel
|
|
153
|
+
this.llmChannel?.off('event:relay.event', this.eventProcessor);
|
|
121
154
|
this.hasSubscribedToEvents = false;
|
|
122
155
|
}
|
|
123
156
|
|
|
@@ -129,42 +162,101 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
129
162
|
}
|
|
130
163
|
|
|
131
164
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* -
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* @
|
|
138
|
-
* @returns {Promise<void>}
|
|
165
|
+
* Attach a pending 'online' listener to a channel for deferred reconciliation.
|
|
166
|
+
* Detaches any existing listener first.
|
|
167
|
+
* @param {LLMChannel} channel - The channel to attach to
|
|
168
|
+
* @param {Function} callback - The callback to run when 'online' fires
|
|
169
|
+
* @private
|
|
170
|
+
* @returns {void}
|
|
139
171
|
*/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
172
|
+
private attachPendingOnline(channel: LLMChannel, callback: () => void): void {
|
|
173
|
+
this.detachPendingOnline();
|
|
174
|
+
this._pendingOnlineListener = callback;
|
|
175
|
+
channel.once('online', this._pendingOnlineListener);
|
|
176
|
+
}
|
|
144
177
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Detach any pending 'online' listener.
|
|
180
|
+
* @private
|
|
181
|
+
* @returns {void}
|
|
182
|
+
*/
|
|
183
|
+
private detachPendingOnline(): void {
|
|
184
|
+
if (this._pendingOnlineListener && this.llmChannel) {
|
|
185
|
+
this.llmChannel.off('online', this._pendingOnlineListener);
|
|
149
186
|
}
|
|
187
|
+
this._pendingOnlineListener = undefined;
|
|
188
|
+
}
|
|
150
189
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Reset announcement state for new connection
|
|
190
|
+
/**
|
|
191
|
+
* Reset announcement state for a new connection.
|
|
192
|
+
* @private
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
private resetAnnounceState(): void {
|
|
159
196
|
this.announceStatus = ANNOUNCE_STATUS.IDLE;
|
|
160
197
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
161
198
|
this.captionServiceId = undefined;
|
|
162
199
|
this.areCaptionsEnabled = false;
|
|
200
|
+
}
|
|
163
201
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Reconcile voicea binding and caption state to match the desired target.
|
|
204
|
+
* This is idempotent - always re-reads targetLLMChannel and current state.
|
|
205
|
+
* A deferred 'online' callback that fires late self-corrects automatically.
|
|
206
|
+
* @private
|
|
207
|
+
* @returns {Promise<void>}
|
|
208
|
+
*/
|
|
209
|
+
private reconcile(): Promise<void> {
|
|
210
|
+
const target = this.targetLLMChannel;
|
|
211
|
+
if (!target) return Promise.resolve();
|
|
212
|
+
|
|
213
|
+
// 1. Rebind relay-event subscription if actual != desired
|
|
214
|
+
if (this.llmChannel !== target) {
|
|
215
|
+
if (this.hasSubscribedToEvents && this.llmChannel) {
|
|
216
|
+
this.llmChannel.off('event:relay.event', this.eventProcessor);
|
|
217
|
+
}
|
|
218
|
+
this.detachPendingOnline();
|
|
219
|
+
this.llmChannel = target;
|
|
220
|
+
this.llmChannel.on('event:relay.event', this.eventProcessor);
|
|
221
|
+
this.hasSubscribedToEvents = true;
|
|
222
|
+
this.resetAnnounceState();
|
|
223
|
+
// Mark caption restoration as pending if captions were subscribed
|
|
224
|
+
this._pendingCaptionRestore = this.keepTranscriptionSubscribed;
|
|
167
225
|
}
|
|
226
|
+
|
|
227
|
+
// 2. Restore captions if pending
|
|
228
|
+
if (!this._pendingCaptionRestore) return Promise.resolve();
|
|
229
|
+
|
|
230
|
+
if (this.isLLMConnected()) {
|
|
231
|
+
// Channel is connected, restore captions immediately
|
|
232
|
+
this._pendingCaptionRestore = false;
|
|
233
|
+
|
|
234
|
+
return this.turnOnCaptions(this.currentSpokenLanguage)
|
|
235
|
+
.then(() => undefined)
|
|
236
|
+
.catch(() => {
|
|
237
|
+
// Best-effort restoration
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// Channel not yet connected - defer until 'online' then re-reconcile.
|
|
241
|
+
// If target changes meanwhile, reconcile() self-corrects.
|
|
242
|
+
this.attachPendingOnline(target, () => {
|
|
243
|
+
this.reconcile();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return Promise.resolve();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Switch to a different LLM channel while preserving caption state.
|
|
251
|
+
* Used when transitioning between main meeting and practice session.
|
|
252
|
+
* This is a thin intent-setter - actual binding happens in reconcile().
|
|
253
|
+
* @param {LLMChannel} newLLMChannel - The new LLM channel to switch to
|
|
254
|
+
* @returns {Promise<void>}
|
|
255
|
+
*/
|
|
256
|
+
public switchLLMChannel(newLLMChannel: LLMChannel): Promise<void> {
|
|
257
|
+
this.targetLLMChannel = newLLMChannel;
|
|
258
|
+
|
|
259
|
+
return this.reconcile();
|
|
168
260
|
}
|
|
169
261
|
|
|
170
262
|
/**
|
|
@@ -187,6 +279,16 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
187
279
|
}
|
|
188
280
|
};
|
|
189
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Process speaker name update and send alert
|
|
284
|
+
* @param {SpeakerNameUpdatePayload} voiceaPayload
|
|
285
|
+
* @returns {void}
|
|
286
|
+
*/
|
|
287
|
+
private processSpeakerNameUpdate = (voiceaPayload: SpeakerNameUpdatePayload): void => {
|
|
288
|
+
// @ts-ignore
|
|
289
|
+
this.emit(EVENT_TRIGGERS.SPEAKER_NAME_UPDATED, voiceaPayload);
|
|
290
|
+
};
|
|
291
|
+
|
|
190
292
|
/**
|
|
191
293
|
* Process Transcript and send alert
|
|
192
294
|
* @param {TranscriptionResponse} voiceaPayload
|
|
@@ -290,7 +392,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
290
392
|
* Indicates whether the LLM channel is connected.
|
|
291
393
|
* @returns {boolean}
|
|
292
394
|
*/
|
|
293
|
-
public isLLMConnected = (): boolean => this.llmChannel
|
|
395
|
+
public isLLMConnected = (): boolean => this.llmChannel?.isConnected() ?? false;
|
|
294
396
|
|
|
295
397
|
public getKeepTranscriptionSubscribed = (): boolean => this.keepTranscriptionSubscribed;
|
|
296
398
|
|
|
@@ -299,9 +401,10 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
299
401
|
* @returns {void}
|
|
300
402
|
*/
|
|
301
403
|
public sendAnnouncement = (): void => {
|
|
404
|
+
const llm = this.requireLLMChannel();
|
|
302
405
|
this.announceStatus = ANNOUNCE_STATUS.JOINING;
|
|
303
|
-
const socket =
|
|
304
|
-
const binding =
|
|
406
|
+
const socket = llm.getSocket();
|
|
407
|
+
const binding = llm.getBinding();
|
|
305
408
|
|
|
306
409
|
const payload = {
|
|
307
410
|
id: `${this.seqNum}`,
|
|
@@ -338,7 +441,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
338
441
|
): Promise<void> =>
|
|
339
442
|
this.request({
|
|
340
443
|
method: 'PUT',
|
|
341
|
-
url: `${this.
|
|
444
|
+
url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
|
|
342
445
|
body: {
|
|
343
446
|
transcribe: {
|
|
344
447
|
spokenLanguage: languageCode,
|
|
@@ -359,8 +462,9 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
359
462
|
return;
|
|
360
463
|
}
|
|
361
464
|
|
|
362
|
-
const
|
|
363
|
-
const
|
|
465
|
+
const llm = this.requireLLMChannel();
|
|
466
|
+
const socket = llm.getSocket();
|
|
467
|
+
const binding = llm.getBinding();
|
|
364
468
|
|
|
365
469
|
socket.send({
|
|
366
470
|
id: `${this.seqNum}`,
|
|
@@ -405,8 +509,9 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
405
509
|
return;
|
|
406
510
|
}
|
|
407
511
|
|
|
408
|
-
const
|
|
409
|
-
const
|
|
512
|
+
const llm = this.requireLLMChannel();
|
|
513
|
+
const socket = llm.getSocket();
|
|
514
|
+
const binding = llm.getBinding();
|
|
410
515
|
|
|
411
516
|
socket?.send({
|
|
412
517
|
id: `${this.seqNum}`,
|
|
@@ -449,7 +554,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
449
554
|
private requestTurnOnCaptions = (languageCode?: string): undefined | Promise<void> => {
|
|
450
555
|
this.captionStatus = TURN_ON_CAPTION_STATUS.SENDING;
|
|
451
556
|
|
|
452
|
-
const locusUrl = this.
|
|
557
|
+
const locusUrl = this.requireLLMChannel().getLocusUrl();
|
|
453
558
|
|
|
454
559
|
const body = {
|
|
455
560
|
transcribe: {caption: true},
|
|
@@ -471,7 +576,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
471
576
|
})
|
|
472
577
|
.catch((error) => {
|
|
473
578
|
this.captionStatus = TURN_ON_CAPTION_STATUS.IDLE;
|
|
474
|
-
throw new Error('turn on captions fail');
|
|
579
|
+
throw new Error('turn on captions fail', {cause: error});
|
|
475
580
|
});
|
|
476
581
|
};
|
|
477
582
|
|
|
@@ -515,7 +620,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
515
620
|
* @returns {Promise}
|
|
516
621
|
*/
|
|
517
622
|
public turnOnCaptions = async (spokenLanguage?: string): Promise<void | undefined> => {
|
|
518
|
-
if (this.
|
|
623
|
+
if (this.isCaptionProcessing()) return undefined;
|
|
519
624
|
|
|
520
625
|
if (!this.isLLMConnected()) {
|
|
521
626
|
throw new Error('can not turn on captions before llm connected');
|
|
@@ -536,7 +641,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
536
641
|
): undefined | Promise<void> => {
|
|
537
642
|
return this.request({
|
|
538
643
|
method: 'PUT',
|
|
539
|
-
url: `${this.
|
|
644
|
+
url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
|
|
540
645
|
body: {
|
|
541
646
|
transcribe: {
|
|
542
647
|
transcribing: activate,
|
|
@@ -564,7 +669,7 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
564
669
|
|
|
565
670
|
return this.request({
|
|
566
671
|
method: 'PUT',
|
|
567
|
-
url: `${this.
|
|
672
|
+
url: `${this.requireLLMChannel().getLocusUrl()}/controls/`,
|
|
568
673
|
body: {
|
|
569
674
|
manualCaption: {
|
|
570
675
|
enable,
|
|
@@ -638,11 +743,12 @@ export class VoiceaChannel extends EventEmitter implements IVoiceaChannel {
|
|
|
638
743
|
} = {}): Promise<void> => {
|
|
639
744
|
if (!this.isLLMConnected()) return;
|
|
640
745
|
|
|
641
|
-
const
|
|
746
|
+
const llm = this.requireLLMChannel();
|
|
747
|
+
const isDataChannelTokenEnabled = await llm.isDataChannelTokenEnabled();
|
|
642
748
|
if (!isDataChannelTokenEnabled) return;
|
|
643
749
|
|
|
644
|
-
const socket =
|
|
645
|
-
const datachannelUrl =
|
|
750
|
+
const socket = llm.getSocket();
|
|
751
|
+
const datachannelUrl = llm.getDatachannelUrl();
|
|
646
752
|
|
|
647
753
|
socket.send({
|
|
648
754
|
id: `${this.seqNum}`,
|
package/src/voicea.types.ts
CHANGED
|
@@ -40,6 +40,21 @@ interface Transcription {
|
|
|
40
40
|
csis: number[];
|
|
41
41
|
last_packet_timestamp_ms: number;
|
|
42
42
|
timestamp: string;
|
|
43
|
+
taggedSpeaker?: {
|
|
44
|
+
speakerId?: string[];
|
|
45
|
+
newName?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface TaggedSpeaker {
|
|
50
|
+
csiId: number;
|
|
51
|
+
speakerId: string;
|
|
52
|
+
newName: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SpeakerNameUpdatePayload {
|
|
56
|
+
id?: string;
|
|
57
|
+
taggedSpeakers: TaggedSpeaker[];
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
/**
|
|
@@ -110,6 +125,10 @@ type MeetingTranscripts = {
|
|
|
110
125
|
[key: string]: string;
|
|
111
126
|
};
|
|
112
127
|
timestamp?: string;
|
|
128
|
+
taggedSpeaker?: {
|
|
129
|
+
speakerId?: string[];
|
|
130
|
+
newName?: string;
|
|
131
|
+
};
|
|
113
132
|
};
|
|
114
133
|
|
|
115
134
|
type MeetingTranscriptPayload = {
|
|
@@ -124,6 +143,8 @@ export type {
|
|
|
124
143
|
TranscriptionResponse,
|
|
125
144
|
Transcription,
|
|
126
145
|
Highlight,
|
|
146
|
+
TaggedSpeaker,
|
|
147
|
+
SpeakerNameUpdatePayload,
|
|
127
148
|
IVoiceaChannel,
|
|
128
149
|
MeetingTranscriptPayload,
|
|
129
150
|
};
|
|
@@ -51,6 +51,18 @@ describe('plugin-voicea', () => {
|
|
|
51
51
|
assert.instanceOf(channel, VoiceaChannel);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it('creates a new VoiceaChannel instance without llmChannel', () => {
|
|
55
|
+
const channel = plugin.createChannel();
|
|
56
|
+
|
|
57
|
+
assert.instanceOf(channel, VoiceaChannel);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('creates a new VoiceaChannel instance with undefined llmChannel', () => {
|
|
61
|
+
const channel = plugin.createChannel(undefined);
|
|
62
|
+
|
|
63
|
+
assert.instanceOf(channel, VoiceaChannel);
|
|
64
|
+
});
|
|
65
|
+
|
|
54
66
|
it('creates independent channels for multiple calls', () => {
|
|
55
67
|
const mockLLMChannel1 = createMockLLMChannel();
|
|
56
68
|
const mockLLMChannel2 = createMockLLMChannel();
|