-
Notifications
You must be signed in to change notification settings - Fork 344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(voicea-highlight): added-highlights #3807
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,9 +42,10 @@ import { | |
EVENT_TRIGGERS as VOICEAEVENTS, | ||
TURN_ON_CAPTION_STATUS, | ||
type MeetingTranscriptPayload, | ||
type MeetingHighlightPayload, | ||
} from '@webex/internal-plugin-voicea'; | ||
|
||
import {processNewCaptions} from './voicea-meeting'; | ||
import {processHighlightCreated, processNewCaptions} from './voicea-meeting'; | ||
|
||
import { | ||
MeetingNotActiveError, | ||
|
@@ -183,6 +184,17 @@ export type CaptionData = { | |
speaker: string; | ||
}; | ||
|
||
export type Highlight = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would need a new type for highlights object. |
||
id: string; | ||
meta: { | ||
label: string; | ||
source: string; | ||
}; | ||
text: string; | ||
timestamp: string; | ||
speaker: any; | ||
}; | ||
|
||
export type Transcription = { | ||
languageOptions: { | ||
captionLanguages?: string; // list of supported caption languages from backend | ||
|
@@ -201,6 +213,7 @@ export type Transcription = { | |
isCaptioning: boolean; | ||
speakerProxy: Map<string, any>; | ||
interimCaptions: Map<string, CaptionData>; | ||
highlights: Array<Highlight>; | ||
}; | ||
|
||
export type LocalStreams = { | ||
|
@@ -680,6 +693,21 @@ export default class Meeting extends StatelessWebexPlugin { | |
interimCaptions: this.transcription.interimCaptions, | ||
}); | ||
}, | ||
[VOICEAEVENTS.HIGHLIGHT_CREATED]: (data: MeetingHighlightPayload) => { | ||
processHighlightCreated({data, meeting: this}); | ||
|
||
LoggerProxy.logger.debug( | ||
`${EventsUtil.getScopeLog({ | ||
file: 'meeting/index', | ||
function: 'setUpVoiceaListeners', | ||
})}event#${EVENT_TRIGGERS.MEETING_HIGHLIGHT_CREATED}` | ||
); | ||
|
||
// @ts-ignore | ||
this.trigger(EVENT_TRIGGERS.MEETING_HIGHLIGHT_CREATED, { | ||
highlights: this.transcription.highlights, | ||
}); | ||
}, | ||
}; | ||
|
||
private addMediaData: { | ||
|
@@ -4944,6 +4972,48 @@ export default class Meeting extends StatelessWebexPlugin { | |
} | ||
} | ||
|
||
/** | ||
* This method will toggle the highlights for the current meeting if the meeting has enabled/supports Webex Assistant | ||
* @param {Boolean} activate flag to enable/disable highlights | ||
* @param {Object} options object with spokenlanguage setting | ||
* @public | ||
* @returns {Promise<void>} a promise to open the WebSocket connection | ||
*/ | ||
public async toggleHighlighting(activate: boolean, options?: {spokenLanguage?: string}) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than a separate |
||
if (this.isJoined() && this.isTranscriptionSupported()) { | ||
LoggerProxy.logger.info( | ||
'Meeting:index#toggleHighlighting --> Attempting to enable highlights!' | ||
); | ||
|
||
try { | ||
if (activate) { | ||
// @ts-ignore | ||
this.webex.internal.voicea.on( | ||
VOICEAEVENTS.HIGHLIGHT_CREATED, | ||
this.voiceaListenerCallbacks[VOICEAEVENTS.HIGHLIGHT_CREATED] | ||
); | ||
} else { | ||
// @ts-ignore | ||
this.webex.internal.voicea.off( | ||
VOICEAEVENTS.HIGHLIGHT_CREATED, | ||
this.voiceaListenerCallbacks[VOICEAEVENTS.HIGHLIGHT_CREATED] | ||
); | ||
} | ||
// @ts-ignore | ||
await this.webex.internal.voicea.toggleTranscribing(activate, options?.spokenLanguage); | ||
} catch (error) { | ||
LoggerProxy.logger.error(`Meeting:index#toggleHighlighting --> ${error}`); | ||
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, { | ||
correlation_id: this.correlationId, | ||
reason: error.message, | ||
stack: error.stack, | ||
}); | ||
} | ||
} else { | ||
throw new Error('This operation is not allowed in your org. Contact org administrator.'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We throw this error if we find that transcriptions are not enabled. Usually happens if the old webex assistant is not enabled for the org. |
||
} | ||
} | ||
|
||
/** | ||
* Callback called when a relay event is received from meeting LLM Connection | ||
* @param {RelayEvent} e Event object coming from LLM Connection | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
import {type MeetingTranscriptPayload} from '@webex/internal-plugin-voicea'; | ||
import { | ||
type MeetingTranscriptPayload, | ||
type MeetingHighlightPayload, | ||
} from '@webex/internal-plugin-voicea'; | ||
|
||
export const getSpeaker = (members, csis = []) => | ||
Object.values(members).find((member: any) => { | ||
|
@@ -118,3 +121,41 @@ export const processNewCaptions = ({ | |
} | ||
transcriptData.interimCaptions[transcriptId] = interimTranscriptionIds; | ||
}; | ||
|
||
export const processHighlightCreated = ({ | ||
data, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We simply process the highlights voicea gives back and pack it into the meetings object as an array. |
||
meeting, | ||
}: { | ||
data: MeetingHighlightPayload; | ||
meeting: any; | ||
}) => { | ||
const transcriptData = meeting.transcription; | ||
|
||
if (!transcriptData.highlights) { | ||
transcriptData.highlights = []; | ||
} | ||
|
||
const csisKey = data.csis && data.csis.length > 0 ? data.csis[0] : undefined; | ||
const {needsCaching, speaker} = getSpeakerFromProxyOrStore({ | ||
meetingMembers: meeting.members.membersCollection.members, | ||
transcriptData, | ||
csisKey, | ||
}); | ||
|
||
if (needsCaching) { | ||
transcriptData.speakerProxy[csisKey] = speaker; | ||
} | ||
|
||
const highlightCreated = { | ||
id: data.highlightId, | ||
meta: { | ||
label: data.highlightLabel, | ||
source: data.highlightSource, | ||
}, | ||
text: data.text, | ||
timestamp: data.timestamp, | ||
speaker, | ||
}; | ||
|
||
meeting.transcription.highlights.push(highlightCreated); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1188,6 +1188,49 @@ describe('plugin-meetings', () => { | |
}); | ||
}); | ||
|
||
describe('#toggleHighlighting', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added tests to check all the flows (toggling). Had to mock a lot of data. |
||
beforeEach(() => { | ||
webex.internal.voicea.on = sinon.stub(); | ||
webex.internal.voicea.off = sinon.stub(); | ||
webex.internal.voicea.listenToEvents = sinon.stub(); | ||
webex.internal.voicea.toggleTranscribing = sinon.stub(); | ||
meeting.isTranscriptionSupported = sinon.stub(); | ||
}); | ||
|
||
afterEach(() => { | ||
sinon.restore(); | ||
}); | ||
|
||
it('should reject if transcription is not supported', (done) => { | ||
meeting.isTranscriptionSupported.returns(false); | ||
|
||
meeting.toggleHighlighting(true).catch((error) => { | ||
assert.equal(error.message, 'This operation is not allowed in your org. Contact org administrator.'); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should enable highlights when meeting is joined and transcription is supported', async () => { | ||
meeting.joinedWith = { | ||
state: 'JOINED', | ||
}; | ||
meeting.isTranscriptionSupported.returns(true); | ||
await meeting.toggleHighlighting(true); | ||
assert.calledOnce(meeting.webex.internal.voicea.toggleTranscribing); | ||
assert.equal(webex.internal.voicea.on.callCount, 1); | ||
}); | ||
|
||
it('should disable highlights when meeting is joined and transcription is supported', async () => { | ||
meeting.joinedWith = { | ||
state: 'JOINED', | ||
}; | ||
meeting.isTranscriptionSupported.returns(true); | ||
await meeting.toggleHighlighting(false) | ||
assert.calledOnce(meeting.webex.internal.voicea.toggleTranscribing); | ||
assert.equal(webex.internal.voicea.off.callCount, 1); | ||
}); | ||
}); | ||
|
||
describe('#setCaptionLanguage', () => { | ||
beforeEach(() => { | ||
meeting.isTranscriptionSupported = sinon.stub(); | ||
|
@@ -1360,6 +1403,14 @@ describe('plugin-meetings', () => { | |
EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED | ||
); | ||
}); | ||
|
||
it('should trigger meeting:highlight-created event', () => { | ||
meeting.voiceaListenerCallbacks[VOICEAEVENTS.HIGHLIGHT_CREATED]({}); | ||
assert.calledWith( | ||
meeting.trigger, | ||
EVENT_TRIGGERS.MEETING_HIGHLIGHT_CREATED | ||
); | ||
}); | ||
}); | ||
|
||
describe('#isReactionsSupported', () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import { | ||
getSpeakerFromProxyOrStore, | ||
processNewCaptions, | ||
processHighlightCreated, | ||
} from '@webex/plugin-meetings/src/meeting/voicea-meeting'; | ||
import {assert} from '@webex/test-helper-chai'; | ||
import { expect } from 'chai'; | ||
|
@@ -253,5 +254,51 @@ describe('plugin-meetings', () => { | |
expect(newCaption.speaker).to.deep.equal(speaker); | ||
}); | ||
}); | ||
|
||
describe('processHighlightCreated', () => { | ||
beforeEach(() => { | ||
fakeVoiceaPayload = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here too - had to mock a lot of values in order to properly unit test the required method. |
||
highlightId: 'highlight1', | ||
highlightLabel: 'Important', | ||
highlightSource: 'User', | ||
text: 'This is a highlight', | ||
timestamp: '2023-10-10T10:00:00Z', | ||
csis: ['3060099329'], | ||
}; | ||
}); | ||
|
||
it('should initialize highlights array if it does not exist', () => { | ||
fakeMeeting.transcription.highlights = undefined; | ||
|
||
processHighlightCreated({data: fakeVoiceaPayload, meeting: fakeMeeting}); | ||
|
||
expect(fakeMeeting.transcription.highlights).to.be.an('array'); | ||
expect(fakeMeeting.transcription.highlights).to.have.lengthOf(1); | ||
}); | ||
|
||
it('should process highlight creation correctly', () => { | ||
processHighlightCreated({data: fakeVoiceaPayload, meeting: fakeMeeting}); | ||
|
||
const csisKey = fakeVoiceaPayload.csis[0]; | ||
const speaker = fakeMeeting.transcription.speakerProxy[csisKey]; | ||
expect(speaker).to.exist; | ||
|
||
const newHighlight = fakeMeeting.transcription.highlights.find( | ||
(highlight) => highlight.id === fakeVoiceaPayload.highlightId | ||
); | ||
expect(newHighlight).to.exist; | ||
expect(newHighlight).to.deep.include({ | ||
id: fakeVoiceaPayload.highlightId, | ||
meta: { | ||
label: fakeVoiceaPayload.highlightLabel, | ||
source: fakeVoiceaPayload.highlightSource, | ||
}, | ||
text: fakeVoiceaPayload.text, | ||
timestamp: fakeVoiceaPayload.timestamp, | ||
}); | ||
|
||
expect(newHighlight.speaker).to.deep.equal(speaker); | ||
}); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New event to be emitted from meeting object to the user.