feat: ensure mic ON on pre-join + voice greeting via Gateway TTS
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
aec15602c2
commit
ad8c858ce4
1 changed files with 80 additions and 2 deletions
|
|
@ -137,6 +137,9 @@ export class BotOrchestrator {
|
||||||
// STEP 1: Navigate to meeting URL and click "Continue on this browser"
|
// STEP 1: Navigate to meeting URL and click "Continue on this browser"
|
||||||
await this._joinProcedure!.startMeetingLauncherFlow(this._meetingUrl);
|
await this._joinProcedure!.startMeetingLauncherFlow(this._meetingUrl);
|
||||||
|
|
||||||
|
// Ensure microphone is ON (required for voice playback)
|
||||||
|
await this._ensureMicOn();
|
||||||
|
|
||||||
// STEP 2: Enter bot name and click "Join now"
|
// STEP 2: Enter bot name and click "Join now"
|
||||||
await this._joinProcedure!.joinMeetingLobbyFlow();
|
await this._joinProcedure!.joinMeetingLobbyFlow();
|
||||||
|
|
||||||
|
|
@ -282,6 +285,9 @@ export class BotOrchestrator {
|
||||||
// Camera stays OFF — no video injection, focus on communication stability
|
// Camera stays OFF — no video injection, focus on communication stability
|
||||||
this._logger.info('Camera left OFF (video disabled for stability)');
|
this._logger.info('Camera left OFF (video disabled for stability)');
|
||||||
|
|
||||||
|
// Ensure microphone is ON (required for voice playback)
|
||||||
|
await this._ensureMicOn();
|
||||||
|
|
||||||
await this._page!.waitForTimeout(2000);
|
await this._page!.waitForTimeout(2000);
|
||||||
|
|
||||||
const joinNowSelectors = [
|
const joinNowSelectors = [
|
||||||
|
|
@ -470,6 +476,67 @@ export class BotOrchestrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the microphone is turned on in the pre-join screen.
|
||||||
|
* Required for voice playback (TTS audio is injected into the mic stream).
|
||||||
|
*
|
||||||
|
* Teams pre-join uses a fui-Switch input:
|
||||||
|
* <input data-tid="toggle-audio" role="switch" type="checkbox" checked>
|
||||||
|
* - checked present = mic ON
|
||||||
|
* - checked absent = mic OFF
|
||||||
|
*/
|
||||||
|
private async _ensureMicOn(): Promise<void> {
|
||||||
|
try {
|
||||||
|
let micToggle = await this._page!.$('input[data-tid="toggle-audio"]');
|
||||||
|
|
||||||
|
if (!micToggle) {
|
||||||
|
const fallbacks = [
|
||||||
|
'[data-tid="toggle-audio"]',
|
||||||
|
'input[role="switch"][title*="microphone" i]',
|
||||||
|
'input[role="switch"][title*="Mikrofon" i]',
|
||||||
|
'input[role="switch"][title*="mic" i]',
|
||||||
|
'input[role="switch"][title*="audio" i]',
|
||||||
|
];
|
||||||
|
for (const sel of fallbacks) {
|
||||||
|
micToggle = await this._page!.$(sel);
|
||||||
|
if (micToggle) {
|
||||||
|
this._logger.info(`Mic toggle found via fallback: ${sel}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!micToggle) {
|
||||||
|
this._logger.warn('Mic toggle not found on pre-join screen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await micToggle.evaluate((el: HTMLInputElement) => ({
|
||||||
|
checked: el.checked,
|
||||||
|
dataCid: el.getAttribute('data-cid') || '',
|
||||||
|
title: el.getAttribute('title') || '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
this._logger.info(`Mic state: checked=${state.checked}, data-cid="${state.dataCid}", title="${state.title}"`);
|
||||||
|
|
||||||
|
if (!state.checked) {
|
||||||
|
await micToggle.click();
|
||||||
|
this._logger.info('Mic toggled ON');
|
||||||
|
await this._page!.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const afterState = await micToggle.evaluate((el: HTMLInputElement) => ({
|
||||||
|
checked: el.checked,
|
||||||
|
dataCid: el.getAttribute('data-cid') || '',
|
||||||
|
}));
|
||||||
|
this._logger.info(`Mic after toggle: checked=${afterState.checked}, data-cid="${afterState.dataCid}"`);
|
||||||
|
} else {
|
||||||
|
this._logger.info('Mic already ON');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this._logger.warn(`Could not toggle mic: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a keepalive timer that periodically moves the mouse and sends
|
* Start a keepalive timer that periodically moves the mouse and sends
|
||||||
* a WebSocket ping. Prevents Teams from detecting the bot as idle
|
* a WebSocket ping. Prevents Teams from detecting the bot as idle
|
||||||
|
|
@ -1055,8 +1122,9 @@ export class BotOrchestrator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a greeting message in the meeting chat after joining.
|
* Send a greeting message in the meeting chat AND via voice after joining.
|
||||||
* Uses the bot's display name and the configured language.
|
* Uses the bot's display name and the configured language.
|
||||||
|
* Voice greeting confirms that the audio pipeline (TTS -> mic) is working.
|
||||||
*/
|
*/
|
||||||
private async _sendJoinGreeting(): Promise<void> {
|
private async _sendJoinGreeting(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1074,8 +1142,18 @@ export class BotOrchestrator {
|
||||||
greeting = `Hello, this is ${firstName}. I'm ready.`;
|
greeting = `Hello, this is ${firstName}. I'm ready.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._logger.info(`Sending join greeting: ${greeting}`);
|
this._logger.info(`Sending join greeting (chat + voice): ${greeting}`);
|
||||||
|
|
||||||
|
// Chat greeting
|
||||||
await this.sendChatMessageToMeeting(greeting);
|
await this.sendChatMessageToMeeting(greeting);
|
||||||
|
|
||||||
|
// Voice greeting — ask Gateway to generate TTS and send back playAudio
|
||||||
|
this._sendToGateway({
|
||||||
|
type: 'voiceGreeting',
|
||||||
|
sessionId: this._sessionId,
|
||||||
|
text: greeting,
|
||||||
|
language: this._options.language || 'de-DE',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._logger.warn('Could not send join greeting:', error);
|
this._logger.warn('Could not send join greeting:', error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue