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