` trees for speaker + text patterns
+
+**Noise filter**: Ignores entries matching known non-transcript patterns (buttons, timestamps without content, single-word UI elements).
+
+### Chat Panel
+
+**Open**: Click `button[id="chat-button"]` — but ONLY if `aria-pressed !== "true"` (prevents toggle-off)
+
+**Scraping target**: Container with `[role="log"]`
+
+**Send messages**: Find CKEditor input (`[data-tid="ckeditor-replyConversation"]` or `div[role="textbox"]`), type text, press Enter.
+
+### Meeting Controls
+
+| Control | Selector | Notes |
+|---------|----------|-------|
+| Mic toggle | `input[data-tid="toggle-audio"]` or `input[role="switch"][title*="mic" i]` | Check `checked` state before toggling |
+| Hangup | `button[id="hangup-button"]` or `#hangup-button` | |
+| More menu | `button[id="callingButtons-showMoreBtn"]` | |
+| Join now | `button[data-tid="prejoin-join-button"]` | |
+
+---
+
+## 13. Deployment
+
+### Infrastructure
+
+| Component | Platform | Details |
+|-----------|----------|---------|
+| Browser Bot | Azure Container Apps | 2 CPU, 4GB RAM, Xvfb for headful mode |
+| Gateway | Azure Container Apps | Shared instance (`cae-poweron-shared`) |
+| Database | Azure Cosmos DB | Sessions, transcripts, responses, system bots |
+| Container Registry | Azure Container Registry | Images tagged by Git SHA |
+
+### CI/CD Pipeline
+
+**Trigger**: Push to `main` branch
+
+**Steps**:
+1. Checkout code
+2. Docker login to ACR
+3. Build image with Playwright base + Xvfb
+4. Push with tags `latest` and `{git-sha}`
+5. Azure login
+6. `az containerapp update` with `--revision-suffix deploy-{sha-prefix}` (forces new revision)
+7. `az containerapp revision restart` on latest revision (ensures container starts)
+
+### Environment Variables (Production)
+
+| Variable | Value |
+|----------|-------|
+| `PORT` | `4100` |
+| `NODE_ENV` | `production` |
+| `BOT_HEADLESS` | `false` (headful via Xvfb) |
+| `GATEWAY_WS_URL` | `wss://gateway-int.poweron-center.net/api/teamsbot/ws` |
+| `DISPLAY` | `:99` (Xvfb) |
+
+---
+
+## 14. Configuration Reference
+
+### Browser Bot Environment
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `PORT` | HTTP server port | `4100` |
+| `GATEWAY_WS_URL` | Gateway WebSocket URL | `wss://gateway-int.poweron-center.net/api/teamsbot/ws` |
+| `BOT_NAME` | Default display name | `PowerOn AI` |
+| `BOT_HEADLESS` | Run headless | `true` (`false` in Docker) |
+| `LOG_LEVEL` | Winston log level | `info` |
+| `SCREENSHOT_ON_ERROR` | Screenshots on errors | `true` |
+
+### Gateway Feature Config
+
+| Field | Default | Range | Description |
+|-------|---------|-------|-------------|
+| `responseMode` | `auto` | — | auto, manual, transcribeOnly |
+| `responseChannel` | `voice` | — | voice, chat, both |
+| `transferMode` | `auto` | — | caption, audio, auto |
+| `language` | `de-DE` | — | BCP-47 language tag |
+| `triggerIntervalSeconds` | `10` | 3–60 | Periodic AI trigger |
+| `triggerCooldownSeconds` | `5` | 1–30 | Min gap between triggers |
+| `contextWindowSegments` | `20` | 5–100 | Transcript lines for AI context |
+
+### Timeouts (Hardcoded in config.ts)
+
+| Timeout | Value | Purpose |
+|---------|-------|---------|
+| `lobbyWait` | 120s | Max time waiting in lobby |
+| `joinTimeout` | 30s | Max time for join flow |
+| `captionsEnable` | 10s | Max time to enable captions |
+| `pageLoad` | 30s | General page load timeout |
+
+---
+
+## 15. Known Constraints & Lessons Learned
+
+### Things That DO NOT Work
+
+| Approach | Why It Fails | Alternative |
+|----------|-------------|-------------|
+| Fake video injection (`--use-file-for-fake-video-capture`) | Crashes Chromium renderer when WebRTC audio starts | Keep camera OFF |
+| Headless Chromium | Teams detects and blocks headless browsers | Use headful + Xvfb |
+| `_setSpokenLanguage()` for live captions | Language dropdown doesn't exist; captions use organizer's language setting | Skip entirely |
+| `str(PythonEnum)` for comparison | Returns `"ClassName.value"`, not `"value"` | Use `enum.value` |
+| Blindly clicking chat button | Toggles panel off if already open | Check `aria-pressed` first |
+| `config.botAccountEmail` | `TeamsbotConfig` doesn't have this field | Use `getattr(self, '_botAccountEmail', None)` |
+| `model_copy(update={...})` for enum fields | Pydantic v2 doesn't coerce strings to enums on copy | Normalize after merge |
+
+### Speech Recognition Artifacts
+
+The bot's name gets mangled by Teams live captions speech recognition. Known variants for "Nyla": Naila, Maila, Neela, Leila, Nila. The system handles this via:
+- **AI prompt**: Explicit warning about phonetic distortion
+- **Trigger logic**: Phonetic similarity check (first letter match, length ±2, character overlap ≥ 60%)
+
+### SSE Event Queue: Create On-Demand
+
+`_emitSessionEvent()` in the Gateway must create the session's event queue (`_sessionEvents[sessionId] = asyncio.Queue()`) if it doesn't already exist. If an event is emitted before the frontend SSE consumer connects, the queue won't exist and events are silently dropped. Always guard with on-demand creation.
+
+### Performance Characteristics
+
+| Operation | Typical Duration |
+|-----------|-----------------|
+| Microsoft login | ~14s |
+| Teams page load after auth | ~10s |
+| Pre-join to in-meeting | ~5s |
+| Caption enable flow | ~8s |
+| AI analysis (GPT-4o-mini) | ~2.5s |
+| TTS generation | ~1s |
+| Full join to first greeting | ~40s |
diff --git a/README.md b/README.md
index b0e7df6..da8acd7 100644
--- a/README.md
+++ b/README.md
@@ -1,67 +1,269 @@
-# Teams Browser Bot Service
+# Teams Bot Service
-Browser-based Microsoft Teams Meeting Bot using Playwright. This service joins Teams meetings via the web interface, captures live captions, and plays TTS audio responses.
-Last rev. 2026-01-15
+AI-powered Microsoft Teams meeting bot. Joins meetings, captures live transcripts, monitors chat, and responds via voice and/or chat messages.
## Architecture
```
-┌─────────────────────────────────────────────────────────────────────────────┐
-│ Browser-based Architecture │
-│ │
-│ ┌───────────────────┐ ┌───────────────────────────────┐ │
-│ │ Gateway │ │ Browser Bot Service │ │
-│ │ (Python) │ │ (Node.js + Playwright) │ │
-│ │ │ │ │ │
-│ │ - STT (Google) │ WebSocket │ - Headless Chrome │ │
-│ │ - AI (OpenAI) │◄──────────────────►│ - Teams Web App │ │
-│ │ - TTS (Google) │ Transcripts │ - Meeting join flow │ │
-│ │ - Session Mgmt │ + TTS Audio │ - Captions scraping │ │
-│ │ │ │ - Audio playback │ │
-│ └───────────────────┘ └───────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────────────────────┘
+ ┌───────────────┐ ┌───────────────────────┐
+ │ │ WebSocket │ │
+ │ Gateway │◄──────────────────────────────────►│ Bot Service │
+ │ │ transcripts, status, chat, │ │
+ │ ● AI engine │ audioChunks, voiceGreeting │ Joins Teams meetings │
+ │ ● TTS │ │ as a participant │
+ │ ● Sessions │ playAudio, sendChatMessage, │ │
+ │ ● Billing │ stopAudio │ Capabilities: │
+ │ │ │ ├ Live transcripts │
+ │ │ HTTP │ ├ Chat messages │
+ │ │────────────────────────────────────► ├ Voice playback │
+ └───────────────┘ join, leave, status │ └ Audio capture │
+ └───────────────────────┘
```
-## Features
+| Path | Protocol | Purpose |
+|------|----------|---------|
+| Gateway ↔ Bot | WebSocket | Real-time transcript, chat, audio, status exchange |
+| Gateway → Bot | HTTP | Session control (join, leave, status) |
-- **Multi-tenant support**: Can join any Teams meeting (not limited to own tenant)
-- **Browser-based**: Uses Teams web app, no Graph Communications SDK needed
-- **Captions scraping**: Captures live captions for transcription
-- **Audio playback**: Plays TTS audio through the browser into the meeting
-- **WebSocket integration**: Real-time communication with Gateway
+## Integration Guide
-## Prerequisites
+### How It Works
-- Node.js 18+
-- Docker (for production deployment)
+1. Gateway sends a **POST** to the Bot with a meeting URL and optional credentials
+2. Bot joins the meeting and appears as a regular participant
+3. A **WebSocket** connection is established for real-time data exchange
+4. Bot streams transcript segments and chat messages to the Gateway
+5. Gateway can send TTS audio or chat responses back into the meeting
-## Quick Start
+### Step 1: Start a Session
-### Local Development
+```http
+POST /api/bot
+Content-Type: application/json
+
+{
+ "sessionId": "550e8400-e29b-41d4-a716-446655440000",
+ "meetingUrl": "https://teams.microsoft.com/meet/123456789?p=abc123",
+ "botName": "AI Assistant",
+ "instanceId": "feature-instance-uuid",
+ "gatewayWsUrl": "wss://gateway.example.com/api/teamsbot/ws",
+ "language": "de-DE",
+ "botAccountEmail": "bot@example.com",
+ "botAccountPassword": "decrypted-password",
+ "transferMode": "caption"
+}
+```
+
+| Field | Required | Description |
+|-------|----------|-------------|
+| `sessionId` | Yes | Unique session UUID (generated by Gateway) |
+| `meetingUrl` | Yes | Teams meeting URL (classic or short format) |
+| `botName` | No | Display name in meeting (default: env `BOT_NAME`) |
+| `instanceId` | No | Feature instance ID for Gateway WebSocket path |
+| `gatewayWsUrl` | No | Gateway WebSocket base URL (default: env `GATEWAY_WS_URL`) |
+| `language` | No | BCP-47 language code (default: `de-DE`) |
+| `botAccountEmail` | No | Microsoft account for authenticated join |
+| `botAccountPassword` | No | Decrypted password for authenticated join |
+| `transferMode` | No | `caption` or `audio` |
+
+If `botAccountEmail` + `botAccountPassword` are provided, the bot joins as an authenticated user. Otherwise, it joins as an anonymous guest.
+
+**Response:**
+
+```json
+{
+ "success": true,
+ "sessionId": "550e8400-e29b-41d4-a716-446655440000",
+ "message": "Bot deployment initiated"
+}
+```
+
+### Step 2: Receive Data via WebSocket
+
+After deployment, the Bot connects to the Gateway at:
+
+```
+wss://{gatewayHost}/api/teamsbot/{instanceId}/bot/ws/{sessionId}
+```
+
+#### Messages: Bot → Gateway
+
+**Transcript segment:**
+
+```json
+{
+ "type": "transcript",
+ "sessionId": "...",
+ "transcript": {
+ "speaker": "Jane Doe",
+ "text": "Hey Bot, can you summarize this?",
+ "timestamp": "2026-02-18T10:30:00.000Z",
+ "isFinal": true
+ }
+}
+```
+
+**Chat message:**
+
+```json
+{
+ "type": "chatMessage",
+ "sessionId": "...",
+ "chat": {
+ "speaker": "Jane Doe",
+ "text": "Please summarize the discussion",
+ "timestamp": "2026-02-18T10:31:00.000Z"
+ }
+}
+```
+
+**Status update:**
+
+```json
+{
+ "type": "status",
+ "sessionId": "...",
+ "status": "joined",
+ "message": "Bot joined the meeting"
+}
+```
+
+Status values: `connecting` | `in_lobby` | `joined` | `left` | `error`
+
+**Voice greeting request** (bot asks Gateway for TTS):
+
+```json
+{
+ "type": "voiceGreeting",
+ "sessionId": "...",
+ "text": "Hello, I am ready.",
+ "language": "de-DE"
+}
+```
+
+**Raw audio chunk** (`transferMode: "audio"` only):
+
+```json
+{
+ "type": "audioChunk",
+ "sessionId": "...",
+ "audio": {
+ "data": "
",
+ "sampleRate": 16000,
+ "format": "pcm16",
+ "timestamp": "2026-02-18T10:30:00.500Z"
+ }
+}
+```
+
+**Keepalive:**
+
+```json
+{ "type": "ping" }
+```
+
+#### Messages: Gateway → Bot
+
+**Play TTS audio:**
+
+```json
+{
+ "type": "playAudio",
+ "sessionId": "...",
+ "audio": {
+ "data": "",
+ "format": "mp3"
+ }
+}
+```
+
+**Send chat message:**
+
+```json
+{
+ "type": "sendChatMessage",
+ "sessionId": "...",
+ "text": "Here is my summary of the discussion..."
+}
+```
+
+**Stop audio playback:**
+
+```json
+{ "type": "stopAudio", "sessionId": "..." }
+```
+
+**Keepalive response:**
+
+```json
+{ "type": "pong" }
+```
+
+### Step 3: Leave the Meeting
+
+```http
+POST /api/bot/:sessionId/leave
+```
+
+```json
+{
+ "success": true,
+ "message": "Leave initiated"
+}
+```
+
+### Step 4: Check Status
+
+```http
+GET /api/bot/:sessionId/status
+```
+
+```json
+{
+ "sessionId": "550e8400-e29b-41d4-a716-446655440000",
+ "state": "in_meeting",
+ "error": null
+}
+```
+
+### Health Check
+
+```http
+GET /health
+```
+
+Returns `200 OK` with `{ "status": "ok", "timestamp": "..." }`.
+
+## Meeting URL Formats
+
+Both formats are supported:
+
+```
+# Classic
+https://teams.microsoft.com/l/meetup-join/19%3ameeting_xxx/0?context=...
+
+# Short
+https://teams.microsoft.com/meet/123456789?p=abc123
+```
+
+## Running the Service
+
+### Local
```bash
-# Install dependencies
npm install
-
-# Install Playwright browsers
-npx playwright install chromium
-
-# Copy and configure environment
-cp .env.sample .env
-# Edit .env with your settings
-
-# Run in development mode
-npm run dev
+cp .env.sample .env # Configure Gateway URL, bot name, etc.
+npm run dev # Dev mode
```
### Docker
```bash
-# Build and run
-docker-compose up --build
+docker build -t teams-bot .
-# Or build image only
-docker build -t teams-browser-bot .
+docker run -p 4100:4100 \
+ -e GATEWAY_WS_URL=wss://gateway.example.com/api/teamsbot/ws \
+ teams-bot
```
## Configuration
@@ -69,106 +271,6 @@ docker build -t teams-browser-bot .
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | HTTP server port | `4100` |
-| `GATEWAY_WS_URL` | Gateway WebSocket URL | `wss://gateway-int.poweron-center.net/api/teamsbot/ws` |
-| `BOT_NAME` | Display name in meetings | `PowerOn AI` |
-| `BOT_HEADLESS` | Run browser headless | `true` |
-| `LOG_LEVEL` | Logging level | `info` |
-| `SCREENSHOT_ON_ERROR` | Take screenshots on errors | `true` |
-
-## API Endpoints
-
-### Health Check
-```
-GET /health
-```
-
-### Deploy Bot
-```
-POST /api/bot
-Content-Type: application/json
-
-{
- "sessionId": "uuid",
- "meetingUrl": "https://teams.microsoft.com/meet/...",
- "botName": "PowerOn AI"
-}
-```
-
-### Leave Meeting
-```
-POST /api/bot/:sessionId/leave
-```
-
-### Get Status
-```
-GET /api/bot/:sessionId/status
-```
-
-## WebSocket Protocol
-
-### Gateway → Bot
-
-```typescript
-// Join a meeting
-{ type: "joinMeeting", sessionId: "uuid", meetingUrl: "...", botName?: "..." }
-
-// Leave meeting
-{ type: "leaveMeeting", sessionId: "uuid" }
-
-// Play audio
-{ type: "playAudio", sessionId: "uuid", audio: { format: "mp3", data: "base64..." } }
-```
-
-### Bot → Gateway
-
-```typescript
-// Transcript
-{ type: "transcript", sessionId: "uuid", transcript: { speaker: "...", text: "...", timestamp: "...", isFinal: true } }
-
-// Status
-{ type: "status", sessionId: "uuid", status: "joined" | "in_lobby" | "left" | "error", message?: "..." }
-```
-
-## Meeting URL Formats
-
-Supports both classic and new (short) URL formats:
-
-```
-# Classic format
-https://teams.microsoft.com/l/meetup-join/19%3ameeting_xxx/0?context=...
-
-# New format (since 2025)
-https://teams.microsoft.com/meet/36438888781520?p=5fGqrujxzewPFjJacW
-```
-
-## Deployment
-
-### Azure Container Instance
-
-```bash
-# Create resource group
-az group create --name rg-teams-bot --location westeurope
-
-# Create container instance
-az container create \
- --resource-group rg-teams-bot \
- --name teams-browser-bot \
- --image /teams-browser-bot:latest \
- --cpu 2 \
- --memory 4 \
- --ports 4100 \
- --environment-variables \
- GATEWAY_WS_URL=wss://gateway-int.poweron-center.net/api/teamsbot/ws \
- BOT_NAME="PowerOn AI"
-```
-
-## Debugging
-
-- Logs are written to `output/logs/`
-- Screenshots (on error) are saved to `output/screenshots/`
-- Set `BOT_HEADLESS=false` for local debugging with visible browser
-
-## Based On
-
-- [Recall.ai Microsoft Teams Meeting Bot](https://github.com/recallai/microsoft-teams-meeting-bot)
-- [Recall.ai Blog: How to build a Microsoft Teams Bot](https://www.recall.ai/blog/how-to-build-a-microsoft-teams-bot)
+| `GATEWAY_WS_URL` | Gateway WebSocket base URL | — |
+| `BOT_NAME` | Default bot display name | — |
+| `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | `info` |
diff --git a/nyla-marketing.html b/nyla-marketing.html
new file mode 100644
index 0000000..704394f
--- /dev/null
+++ b/nyla-marketing.html
@@ -0,0 +1,321 @@
+
+
+
+
+
+Nyla — Your AI Colleague in Every Meeting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
New from PowerOn
+
Meet Nyla
+
She joins your Microsoft Teams meeting as a real colleague. She listens. She speaks. She answers your questions — live.
+
Discover Nyla
+
+
+
+
+
+
+
Why this changes everything
+
No one in the meeting knows it's AI.
+
+ Nyla appears as a real person in the participant list. No "Bot" badge. No "Guest" tag. No IT setup at your end. Just share the meeting link.
+
+
+
+
+
Other AI meeting tools
+
×
Show up as "Bot" or "Guest" in the roster
+
×
Need your IT admin to allow guest access
+
×
Require app installation or licenses
+
×
Only transcribe — can't speak or act
+
+
+
Nyla
+
✓
Appears as a real person — no labels
+
✓
Zero setup for you — no admin, no config
+
✓
Nothing to install. No licenses needed.
+
✓
Speaks, chats, answers questions — live
+
+
+
+
+
+
+
+
+
+
+
What she can do
+
Not a recorder. A participant.
+
Nyla listens, speaks, answers, analyzes, and summarizes — while the meeting is still running.
+
+
+
+
+
Speaks in the meeting
+
Participants hear Nyla speak — just like any other colleague.
+
+
+
+
Chats in real-time
+
Reads and writes in the meeting chat. Voice, text, or both.
+
+
+
+
Answers questions live
+
"Nyla, summarize what we discussed." She responds immediately.
+
+
+
+
Analyzes continuously
+
Real-time AI running throughout the meeting. Not just after.
+
+
+
+
Full transcript
+
Every word, every speaker, properly attributed. Streamed live.
+
+
+
+
Meeting summary
+
Key decisions, action items, topics — delivered when the meeting ends.
+
+
+
+
+
+
+
+
+
The comparison
+
See how Nyla stacks up.
+
+
+
+
+
+ | What you get |
+ Nyla |
+ Fireflies.ai |
+ Read.ai |
+ Otter.ai |
+ MS Copilot |
+
+
+
+
+ | Appears as a real colleague |
+ ✓ |
+ Guest label |
+ App label |
+ Bot label |
+ Native |
+
+
+ | Zero IT setup at your end |
+ ✓ |
+ × |
+ × |
+ Partial |
+ × |
+
+
+ | Speaks in the meeting |
+ ✓ |
+ ✓ |
+ × |
+ × |
+ × |
+
+
+ | Chats in the meeting |
+ ✓ |
+ ✓ |
+ × |
+ × |
+ Text only |
+
+
+ | Answers questions on demand |
+ ✓ |
+ ✓ |
+ × |
+ × |
+ Partial |
+
+
+ | Runs commands in Teams |
+ ✓ |
+ × |
+ × |
+ × |
+ Partial |
+
+
+ | Works with any organization |
+ ✓ |
+ Admin needed |
+ App install |
+ Partial |
+ × |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+