From ad8c858ce407090d6c77c0d558144304fcf5711a Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 18 Feb 2026 00:17:41 +0100 Subject: [PATCH] feat: ensure mic ON on pre-join + voice greeting via Gateway TTS Co-authored-by: Cursor --- src/bot/orchestrator.ts | 82 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index 3389bc0..ac9d25a 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -137,6 +137,9 @@ export class BotOrchestrator { // STEP 1: Navigate to meeting URL and click "Continue on this browser" 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" await this._joinProcedure!.joinMeetingLobbyFlow(); @@ -282,6 +285,9 @@ export class BotOrchestrator { // Camera stays OFF — no video injection, focus on communication 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); 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: + * + * - checked present = mic ON + * - checked absent = mic OFF + */ + private async _ensureMicOn(): Promise { + 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 * 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. + * Voice greeting confirms that the audio pipeline (TTS -> mic) is working. */ private async _sendJoinGreeting(): Promise { try { @@ -1074,8 +1142,18 @@ export class BotOrchestrator { 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); + + // 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) { this._logger.warn('Could not send join greeting:', error); }