Skip to content
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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@webex/plugin-meetings/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ export const EVENT_TRIGGERS = {
MEETING_STOPPED_RECEIVING_TRANSCRIPTION: 'meeting:receiveTranscription:stopped',
MEETING_MANUAL_CAPTION_UPDATED: 'meeting:manualCaptionControl:updated',
MEETING_CAPTION_RECEIVED: 'meeting:caption-received',
MEETING_HIGHLIGHT_CREATED: 'meeting:highlight-created',
Copy link
Contributor Author

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.

};

export const EVENT_TYPES = {
Expand Down
72 changes: 71 additions & 1 deletion packages/@webex/plugin-meetings/src/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -183,6 +184,17 @@ export type CaptionData = {
speaker: string;
};

export type Highlight = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand All @@ -201,6 +213,7 @@ export type Transcription = {
isCaptioning: boolean;
speakerProxy: Map<string, any>;
interimCaptions: Map<string, CaptionData>;
highlights: Array<Highlight>;
};

export type LocalStreams = {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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}) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than a separate stop or start method, we can use toggle which enabled users to toggle the highlighting setting. This way we can also compact all the logic inside of this method.

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.');
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down
43 changes: 42 additions & 1 deletion packages/@webex/plugin-meetings/src/meeting/voicea-meeting.ts
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) => {
Expand Down Expand Up @@ -118,3 +121,41 @@ export const processNewCaptions = ({
}
transcriptData.interimCaptions[transcriptId] = interimTranscriptionIds;
};

export const processHighlightCreated = ({
data,
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
};
51 changes: 51 additions & 0 deletions packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,49 @@ describe('plugin-meetings', () => {
});
});

describe('#toggleHighlighting', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
Expand Down Expand Up @@ -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', () => {
Expand Down
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';
Expand Down Expand Up @@ -253,5 +254,51 @@ describe('plugin-meetings', () => {
expect(newCaption.speaker).to.deep.equal(speaker);
});
});

describe('processHighlightCreated', () => {
beforeEach(() => {
fakeVoiceaPayload = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
});
});
});
});
Loading