From e6983b36d0cf204abb8a2c9d51be2eb01a70840f Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 20 Jan 2026 00:41:50 +0000 Subject: [PATCH] feat(examples): add say-server - streaming Pocket TTS with karaoke highlighting (#304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add say-server: streaming TTS MCP App with karaoke UI - Streaming TTS MCP App with real-time word highlighting - React widget with pause/resume controls - Debounced model context updates - Self-contained server.py for standalone uv run Co-authored-by: Claude * Update server.py * fix(say-server): fix React/Zod version conflicts in widget - Pin Zod to v3.25.1 via esm.sh deps param (fixes .custom() error) - Add external=react,react-dom to prevent duplicate React instances - Update React from 19.1.0 to 19.2.0 - Import React for Babel's classic JSX transform - Consolidate imports: only use @modelcontextprotocol/ext-apps/react - Use Babel from unpkg.com instead of esm.sh - Simplify resource decorator with meta= parameter - Update CSP to allow both esm.sh and unpkg.com * fix(say-server): show play button when paused, restart when finished * feat(say-server): add fullscreen button, remove inline scroll - Add fullscreen toggle button (appears on hover when available) - Remove max-height/scroll in inline mode for cleaner display - Enable scrolling only in fullscreen mode - Track displayMode and fullscreenAvailable state - Handle onHostContextChanged to detect fullscreen support * fix(say-server): fix fullscreen button visibility and prevent multiple audio streams - Check initial host context for fullscreen availability (not just updates) - Add audioOperationInProgressRef guard to prevent concurrent audio operations - Prevents race conditions from rapid clicks/double-clicks * fix(qr-server): use meta parameter instead of manual handler patching Simplified CSP metadata handling to match say-server's approach: - Use meta parameter on @mcp.resource decorator directly - Removed custom _read_resource_with_meta handler and handler replacement * fix(say-server): simplify controls to prevent multiple audio streams - Remove onClick from text display (was conflicting with double-click) - Always show overlay button (with pause icon when playing) - Lower opacity (0.3) when playing, full on hover - Button shows: ▢️ (play), ⏸️ (pause), πŸ”„ (restart) - Double-click text to restart still works - Eliminates click/dblclick race condition * fix(say-server): only show pause button on hover - Play/restart buttons visible normally - Pause button hidden until hover (doesn't block karaoke text) * fix(say-server): fix partial input handling and session management - Add promise lock to initTTSQueue to prevent concurrent initialization - Reset queueIdRef and lastTextRef in ontoolresult for next tool call - Close existing AudioContext when starting new session - Reset all playback state (chunks, timings, position) on new session - Fixes: text deltas not sent when new tool call has shorter text * fix(say-server): detect new sessions by text comparison, not queueIdRef reset - Don't reset queueIdRef in ontoolresult - audio should keep playing - Detect new sessions in ontoolinputpartial/ontoolinput by checking if new text starts with lastTextRef (continuation) or not (new session) - Only reset and create new queue when genuinely new session detected - Fixes: pause/play was creating new streams because queueIdRef was reset * feat(say-server): add theming, floating button, and keyboard shortcuts Theming: - Use CSS variables (--font-sans, --color-text-primary/secondary) with fallbacks - Apply host fonts via applyHostFonts() on connect and context changes - Apply host style variables via applyHostStyleVariables() - Apply document theme via applyDocumentTheme() Floating button: - Button follows cursor position (below current line being spoken) - Uses invisible cursor marker element for accurate positioning - Shows ⏸️ when playing, ▢️ when paused, πŸ”„ when finished - Low opacity when playing, full on hover Keyboard shortcuts: - Space: toggle play/pause - Enter: toggle fullscreen (when available) UI changes: - Large overlay only shown for initial play (idle state) - Floating button used during playback for less intrusive controls * docs(say-server): add TODO items for future improvements * fix(say-server): simplify to fixed play button at top right - Remove floating cursor-following button (was too complex) - Fixed play button at top-right corner - Low opacity when playing, full on hover - Keep theming and keyboard shortcuts (Space/Enter) - Add say-server to E2E tests * test(e2e): add say-server to tests but skip in CI Say-server requires Pocket TTS model (~500MB) and GPU support, which isn't available in the Docker CI environment. Skip it for now. * test(e2e): add say-server and qr-server to screenshot generation - Remove say-server from SKIP_SERVERS (widget works without TTS model) - Add say-server masks for play buttons - Add both say-server and qr-server to grid screenshot generator * fix(say-server): restore working play/pause overlay, keep theming and keyboard shortcuts Reverted to working commit 413beca and carefully reapplied: - CSS variables for theming (--font-sans, --color-text-primary/secondary) - Dark mode support via @media (prefers-color-scheme) - Host theming via applyHostStyleVariables/applyHostFonts/applyDocumentTheme - Keyboard shortcuts: Space = play/pause, Enter = fullscreen Preserved working behavior: - Play overlay visible when not playing, hover to show when playing - Pause on click works correctly - Karaoke highlighting works * fix(say-server): minimal CSS-only theming, no JS changes from working version Only CSS changes from working commit 413beca: - Added CSS variables for theming foundation - No JavaScript changes (reverted theming calls and keyboard shortcuts) This isolates whether the issue is CSS or JS related. * feat(say-server): add autoPlay param and Reset button - Add autoPlay parameter (default: true) with Pydantic Field description - Description visible in MCP tool schema: 'browsers may block autoplay' - Add Reset button next to Fullscreen button (bottom right) - Both buttons share controlBtn base styles, shown on hover - Widget reads autoPlay from tool input params * fix(say-server): update FastMCP API for new SDK version Python SDK refactored transport params from constructor to run(): - Remove port, stateless_http from FastMCP() constructor - Pass stateless_http=True to streamable_http_app() instead * debug(say-server): add console logs to trace audio stream issue Logging added to: - togglePlayPause (all branches) - initTTSQueue (entry and AudioContext creation) - restartPlayback (entry) - ontoolinputpartial/ontoolinput (entry and new session detection) Open browser DevTools console and reproduce the issue - logs will show what's being called. * feat(say-server): add play/pause buttons in top-left and bottom-right corners - Top left: playPauseTopBtn for quick access - Bottom right: playPauseBtn alongside reset and fullscreen - Both show on hover with same styling as other control buttons - Icons: ▢️ (play), ⏸️ (pause), πŸ”„ (restart when finished) * fix(say-server): simplify UI and fix pause/resume audio duplication - Remove central play overlay, use single toolbar in top-right - Fix audio duplication on pause/resume (only defer to pendingChunks for initial autoplay) - Remove double-click to restart (use toolbar restart button) - Sync displayMode when host changes it externally - Click on text to play/pause * feat(say-server): add voice selection support - Add list_voices tool to show available voices - Add voice parameter to say tool (predefined names, HuggingFace URLs, or local paths) - Predefined voices: alba, marius, javert, jean, cosette, eponine, azelma, fantine - Update README with voice documentation * docs(say-server): improve tool description for better LLM triggering - Add English-only constraint prominently in first line and note - List common trigger patterns (say, speak, read aloud, etc.) - Update text parameter description to mention English - Keep description concise for quick LLM scanning * feat(say-server): add speak lock for multi-widget coordination When multiple TTS widgets exist, only one plays at a time: - Widget announces to localStorage when starting playback - Playing widgets poll localStorage every 200ms - If another widget starts, previous one pauses automatically - Lock is cleared on pause/finish/teardown Uses _meta.widgetUUID in tool result for widget identification. * update package.json of say * feat(say-server): add screenshots and update README gallery - Add say-server to demo gallery with 300x300 grid-cell.png - Fix qr-server grid-cell.png dimensions (was 672x668, now 300x300) - Add golden snapshot for say-server E2E tests - Add full screenshot and grid-cell thumbnail for say-server πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * style(say-server): apply prettier formatting - Add blank lines in README for better readability - Add trailing newline to package.json πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * feat(say-server): add Escape key to exit fullscreen and update docs - Add onKeyDown handler to main element for Escape key - Update README with full list of MCP App features: - Model context updates - Native theming - Fullscreen mode - Multi-widget speak lock - Update server.py docstring with feature documentation πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix(say-server): add queue timeout to prevent infinite polling The TTS queue could hang forever if: - The LLM stops generating mid-stream (no end signal) - User cancels before tool completes - Network interruption prevents end_tts_queue call Changes: - Add last_activity timestamp to track queue activity - Add 30-second timeout (QUEUE_TIMEOUT_SECONDS) - Poll with 5-second timeout, checking for stale queues - Mark stale queues as "error" so widget stops polling - Return error message in poll_tts_audio response - Add error logging in widget polling loop This prevents tool calls from running forever when queues are abandoned. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * debug(say-server): add logging to track queue lifecycle Add console logging to debug why end_tts_queue might not be called: Widget-side: - Log queueId in ontoolinputpartial, ontoolinput, ontoolresult - Log when new session is detected (queue reset) - Log when initTTSQueue fails - Log when end_tts_queue is called/not called Server-side: - Log when end_tts_queue is called - Log warnings for unknown queues - Log info for already-ended queues This will help identify if the issue is: - User interruption (ontoolresult never called) - Session reset (queueId becomes null) - Host bug (callback dropped) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * perf(say-server): add initial delay before polling Add 150ms delay before first poll to give TTS model time to generate the first audio chunk. This reduces unnecessary polling and server load. Current backoff after that: - 30ms if chunks received (keep streaming fast) - 80ms if no chunks (wait longer for generation) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix(say-server): use adaptive polling instead of fixed initial delay Previous approach: Always wait 150ms before first poll Problem: Even when chunks are ready, we wait unnecessarily New approach: Adaptive polling - Start polling immediately (no initial delay) - 20ms backoff when receiving chunks (fast streaming) - Exponential backoff when no chunks: 50ms β†’ 100ms β†’ 150ms max - Reset backoff when chunks start flowing This reduces latency when TTS is fast while being polite when it's slow. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * feat(say-server): add speaker icon to server info Uses SVG data URI for a speaker with sound waves icon. * docs(say-server): add comprehensive third-party licensing information - Expand License section in README with component attribution table - Add voice collection license matrix with commercial use indicators - Add warning about non-commercial expresso/ears voice collections - Clarify NON-COMMERCIAL restriction in list_voices() output - Use consistent CC-BY formatting (hyphenated) * Update last activity to prevent timeout during active polling * feat(say-server): add info button, SVG icons, and UI fixes - Replace emoji play/pause buttons with neutral white SVG icons - Add (i) info button in bottom-right with attribution popup - Use app.openLink() API for external links in popup - Add dynamic padding to fit popup when shown - Make create_tts_queue async to fix AnyIO event loop error - Add better error logging for TTS queue initialization - Update README with openLink feature documentation - Remove unnecessary HuggingFace login prerequisites * docs(say-server): document multi-widget speak lock mechanism * prettier:fix --------- Co-authored-by: Claude --- .gitignore | 2 + README.md | 4 +- examples/say-server/.gitignore | 2 + examples/say-server/README.md | 175 ++ examples/say-server/grid-cell.png | Bin 0 -> 38830 bytes examples/say-server/mcp-app.html | 14 + examples/say-server/package.json | 17 + examples/say-server/screenshot.png | Bin 0 -> 22919 bytes examples/say-server/server.py | 1457 +++++++++++++++++ package-lock.json | 42 + tests/e2e/generate-grid-screenshots.spec.ts | 1 + tests/e2e/servers.spec.ts | 1 + .../servers.spec.ts-snapshots/say-server.png | Bin 0 -> 39655 bytes 13 files changed, 1713 insertions(+), 2 deletions(-) create mode 100644 examples/say-server/.gitignore create mode 100644 examples/say-server/README.md create mode 100644 examples/say-server/grid-cell.png create mode 100644 examples/say-server/mcp-app.html create mode 100644 examples/say-server/package.json create mode 100644 examples/say-server/screenshot.png create mode 100755 examples/say-server/server.py create mode 100644 tests/e2e/servers.spec.ts-snapshots/say-server.png diff --git a/.gitignore b/.gitignore index 1d78df5..9c6a137 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ intermediate-findings/ # Playwright playwright-report/ test-results/ +__pycache__/ +*.pyc \ No newline at end of file diff --git a/README.md b/README.md index 6057fe1..ece4234 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ Or edit your `package.json` manually: | [**Scenario Modeler**](examples/scenario-modeler-server) | [**Budget Allocator**](examples/budget-allocator-server) | [**Customer Segmentation**](examples/customer-segmentation-server) | | [![System Monitor](examples/system-monitor-server/grid-cell.png "Real-time OS metrics")](examples/system-monitor-server) | [![Transcript](examples/transcript-server/grid-cell.png "Live speech transcription")](examples/transcript-server) | [![Video Resource](examples/video-resource-server/grid-cell.png "Binary video via MCP resources")](examples/video-resource-server) | | [**System Monitor**](examples/system-monitor-server) | [**Transcript**](examples/transcript-server) | [**Video Resource**](examples/video-resource-server) | -| [![PDF Server](examples/pdf-server/grid-cell.png "Interactive PDF viewer with chunked loading")](examples/pdf-server) | [![QR Code](examples/qr-server/grid-cell.png "QR code generator")](examples/qr-server) | | -| [**PDF Server**](examples/pdf-server) | [**QR Code (Python)**](examples/qr-server) | | +| [![PDF Server](examples/pdf-server/grid-cell.png "Interactive PDF viewer with chunked loading")](examples/pdf-server) | [![QR Code](examples/qr-server/grid-cell.png "QR code generator")](examples/qr-server) | [![Say Demo](examples/say-server/grid-cell.png "Text-to-speech demo")](examples/say-server) | +| [**PDF Server**](examples/pdf-server) | [**QR Code (Python)**](examples/qr-server) | [**Say Demo**](examples/say-server) | ### Starter Templates diff --git a/examples/say-server/.gitignore b/examples/say-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/say-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/say-server/README.md b/examples/say-server/README.md new file mode 100644 index 0000000..b6629ef --- /dev/null +++ b/examples/say-server/README.md @@ -0,0 +1,175 @@ +# Say Server - Streaming TTS MCP App + +A real-time text-to-speech MCP App with karaoke-style text highlighting, powered by [Kyutai's Pocket TTS](https://github.com/kyutai-labs/pocket-tts). + +## MCP App Features Demonstrated + +This example showcases several MCP App capabilities: + +- **Single-file executable**: Python server with embedded React UI - no build step required +- **Partial tool inputs** (`ontoolinputpartial`): Widget receives streaming text as it's being generated +- **Queue-based streaming**: Demonstrates how to stream text out and audio in via a polling tool (adds text to an input queue, retrieves audio chunks from an output queue) +- **Model context updates**: Widget updates the LLM with playback progress ("Playing: ...snippet...") +- **Native theming**: Uses CSS variables for automatic dark/light mode adaptation +- **Fullscreen mode**: Toggle fullscreen via `requestDisplayMode()` API, press Escape to exit +- **Multi-widget speak lock**: Coordinates multiple TTS widgets via localStorage so only one plays at a time +- **Hidden tools** (`visibility: ["app"]`): Private tools only accessible to the widget, not the model +- **External links** (`openLink`): Attribution popup uses `app.openLink()` to open external URLs +- **CSP metadata**: Resource declares required domains (`esm.sh`) for in-browser transpilation + +## Features + +- **Streaming TTS**: Audio starts playing as text is being generated +- **Karaoke highlighting**: Words are highlighted in sync with speech +- **Interactive controls**: Click to pause/resume, double-click to restart +- **Low latency**: Uses a polling-based queue for minimal delay + +## Prerequisites + +- [uv](https://docs.astral.sh/uv/getting-started/installation/) - fast Python package manager +- A CUDA GPU (recommended) or CPU with sufficient RAM (~2GB for model) + +## Quick Start + +The server is a single self-contained Python file that can be run directly with `uv`: + +```bash +# Run directly (uv auto-installs dependencies) +uv run examples/say-server/server.py +``` + +The server will be available at `http://localhost:3109/mcp`. + +## Running with Docker + +Run directly from GitHub using the official `uv` Docker image. Mount your HuggingFace cache to avoid re-downloading the model: + +```bash +docker run --rm -it \ + -p 3109:3109 \ + -v ~/.cache/huggingface:/root/.cache/huggingface \ + -e HF_HOME=/root/.cache/huggingface \ + ghcr.io/astral-sh/uv:debian \ + uv run https://raw.githubusercontent.com/modelcontextprotocol/ext-apps/main/examples/say-server/server.py +``` + +For GPU support, add `--gpus all` (requires [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html)). + +## Usage + +### With Claude Desktop + +Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "say": { + "command": "uv", + "args": ["run", "server.py", "--stdio"], + "cwd": "/path/to/examples/say-server" + } + } +} +``` + +### With MCP Clients + +Connect to `http://localhost:3109/mcp` and call the `say` tool: + +```json +{ + "name": "say", + "arguments": { + "text": "Hello, world! This is a streaming TTS demo." + } +} +``` + +## Available Voices + +The default voice is `cosette`. Use the `list_voices` tool or pass a `voice` parameter to `say`: + +### Predefined Voices + +- `alba`, `marius`, `javert`, `jean` - from [alba-mackenna](https://huggingface.co/kyutai/tts-voices/tree/main/alba-mackenna) (CC BY 4.0) +- `cosette`, `eponine`, `azelma`, `fantine` - from [VCTK dataset](https://huggingface.co/kyutai/tts-voices/tree/main/vctk) (CC BY 4.0) + +### Custom Voices + +You can also use HuggingFace URLs or local file paths: + +```json +{"text": "Hello!", "voice": "hf://kyutai/tts-voices/voice-donations/alice.wav"} +{"text": "Hello!", "voice": "/path/to/my-voice.wav"} +``` + +See the [kyutai/tts-voices](https://huggingface.co/kyutai/tts-voices) repository for more voice collections + +## Architecture + +The entire server is contained in a single `server.py` file: + +1. **`say` tool**: Public tool that triggers the widget with text to speak +2. **Private tools** (`create_tts_queue`, `add_tts_text`, `poll_tts_audio`, etc.): Hidden from the model, only callable by the widget +3. **Embedded React widget**: Uses [Babel standalone](https://babeljs.io/docs/babel-standalone) for in-browser JSX transpilation - no build step needed +4. **TTS backend**: Manages per-request audio queues using Pocket TTS + +The widget communicates with the server via MCP tool calls: + +- Receives streaming text via `ontoolinputpartial` callback +- Incrementally sends new text to the server as it arrives (via `add_tts_text`) +- Polls for generated audio chunks while TTS runs in parallel +- Plays audio via Web Audio API with synchronized text highlighting + +## Multi-Widget Speak Lock + +When multiple TTS widgets exist in the same browser (e.g., multiple chat messages each with their own say widget), they coordinate via localStorage to ensure only one plays at a time: + +1. **Unique Widget IDs**: Each widget receives a UUID via `toolResult._meta.widgetUUID` +2. **Announce on Play**: When starting, a widget writes `{uuid, timestamp}` to `localStorage["mcp-tts-playing"]` +3. **Poll for Conflicts**: Every 200ms, playing widgets check if another widget took the lock +4. **Yield Gracefully**: If another widget started playing, pause and yield +5. **Clean Up**: On pause/finish, clear the lock (only if owned) + +This "last writer wins" protocol ensures a seamless experience: clicking play on any widget immediately pauses others, without requiring cross-iframe postMessage coordination. + +## TODO + +- Persist caret position in localStorage (resume from where you left off) +- Click anywhere in text to move the cursor/playback position + +## Credits + +This project uses [Pocket TTS](https://github.com/kyutai-labs/pocket-tts) by [Kyutai](https://kyutai.org/) - a fantastic open-source text-to-speech model. Thank you to the Kyutai team for making this technology available! + +The server includes modified Pocket TTS code to support streaming text input (text can be fed incrementally while audio generation runs in parallel). A PR contributing this functionality back to the original repo is planned. + +## License + +This example is MIT licensed. + +### Third-Party Licenses + +This project uses the following open-source components: + +| Component | License | Link | +| --------------------------------------------------------------------- | ----------------- | ---------------------------- | +| [pocket-tts](https://github.com/kyutai-labs/pocket-tts) | MIT | Python TTS library | +| [Kyutai TTS model](https://huggingface.co/kyutai/tts-0.75b-en-public) | CC-BY 4.0 | Text-to-speech model weights | +| [kyutai/tts-voices](https://huggingface.co/kyutai/tts-voices) | Mixed (see below) | Voice prompt files | + +### Voice Collection Licenses + +The predefined voices in this example use **CC-BY 4.0** licensed collections: + +| Collection | License | Commercial Use | +| --------------- | ------------------- | ------------------------- | +| alba-mackenna | CC-BY 4.0 | βœ… Yes (with attribution) | +| vctk | CC-BY 4.0 | βœ… Yes (with attribution) | +| cml-tts/fr | CC-BY 4.0 | βœ… Yes (with attribution) | +| voice-donations | CC0 (Public Domain) | βœ… Yes | +| **expresso** | CC-BY-NC 4.0 | ❌ Non-commercial only | +| **ears** | CC-BY-NC 4.0 | ❌ Non-commercial only | + +⚠️ **Note**: If you use voices from the `expresso/` or `ears/` collections, your use is restricted to non-commercial purposes. diff --git a/examples/say-server/grid-cell.png b/examples/say-server/grid-cell.png new file mode 100644 index 0000000000000000000000000000000000000000..ff2e85a95477547e94810db11ff3e8b8b7a6ce11 GIT binary patch literal 38830 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 + + + + + + Say Widget + + + +
+ + + diff --git a/examples/say-server/package.json b/examples/say-server/package.json new file mode 100644 index 0000000..a752ef7 --- /dev/null +++ b/examples/say-server/package.json @@ -0,0 +1,17 @@ +{ + "name": "@modelcontextprotocol/server-say", + "version": "0.4.1", + "private": true, + "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/ext-apps", + "directory": "examples/say-server" + }, + "license": "MIT", + "scripts": { + "start": "uv run server.py", + "dev": "uv run server.py", + "build": "echo 'No build step needed for Python server'" + } +} diff --git a/examples/say-server/screenshot.png b/examples/say-server/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..0ac8e6cc1f17a1d150ed4f0e957a0bcd46486bbb GIT binary patch literal 22919 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.34.0", +# "starlette>=0.46.0", +# "pocket-tts>=1.0.1", +# ] +# /// +""" +Say Demo - MCP App for streaming text-to-speech. + +This MCP server provides a "say" tool that speaks text using TTS. +The widget receives streaming partial input and starts speaking immediately. + +Architecture: +- The `say` tool itself is a no-op - it just triggers the widget +- The widget uses `ontoolinputpartial` to receive text as it streams +- Widget calls private tools to create TTS queue, add text, and poll audio +- Audio plays in the widget using Web Audio API +- Model context updates show playback progress to the LLM +- Native theming adapts to dark/light mode automatically +- Fullscreen mode with Escape key to exit +- Multi-widget speak lock coordinates playback across instances + +Usage: + # Start the MCP server + python server.py + + # Or with stdio transport (for Claude Desktop) + python server.py --stdio +""" +from __future__ import annotations +import asyncio +import base64 +import logging +import os +import sys +import time +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import Annotated, Literal +from pydantic import Field + +import torch +import uvicorn +from mcp.server.fastmcp import FastMCP +from mcp import types +from mcp.types import Icon +from starlette.middleware.cors import CORSMiddleware + +from pocket_tts.models.tts_model import TTSModel, prepare_text_prompt +from pocket_tts.default_parameters import DEFAULT_AUDIO_PROMPT + +logger = logging.getLogger(__name__) + +WIDGET_URI = "ui://say-demo/widget.html" +HOST = os.environ.get("HOST", "0.0.0.0") +PORT = int(os.environ.get("PORT", "3109")) + +# Speaker icon as SVG data URI +SPEAKER_ICON = Icon( + src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolygon points='11 5 6 9 2 9 2 15 6 15 11 19 11 5'/%3E%3Cpath d='M15.54 8.46a5 5 0 0 1 0 7.07'/%3E%3Cpath d='M19.07 4.93a10 10 0 0 1 0 14.14'/%3E%3C/svg%3E", + mimeType="image/svg+xml", +) + +mcp = FastMCP("Say Demo", icons=[SPEAKER_ICON]) + +# Global TTS model (loaded on startup) +tts_model: TTSModel | None = None + + +# ------------------------------------------------------ +# TTS Queue State Management +# ------------------------------------------------------ + +@dataclass +class AudioChunkData: + """Audio chunk with timing metadata.""" + index: int + audio_base64: str + char_start: int + char_end: int + duration_ms: float + + +@dataclass +class TTSQueueState: + """State for a TTS generation queue.""" + id: str + voice: str + sample_rate: int + status: Literal["active", "complete", "error"] = "active" + error_message: str | None = None + + # Text queue + text_queue: asyncio.Queue = field(default_factory=asyncio.Queue) + end_signaled: bool = False + + # Audio output + audio_chunks: list[AudioChunkData] = field(default_factory=list) + chunks_delivered: int = 0 + + # Tracking + created_at: float = field(default_factory=time.time) + last_activity: float = field(default_factory=time.time) # Last text or end signal + lock: asyncio.Lock = field(default_factory=asyncio.Lock) + task: asyncio.Task | None = None + + +# Active TTS queues +tts_queues: dict[str, TTSQueueState] = {} + +# Queue timeout: if no activity for this long, mark as error +QUEUE_TIMEOUT_SECONDS = 30 + + +# ------------------------------------------------------ +# Public Tool: say +# ------------------------------------------------------ + +DEFAULT_TEXT = """Hello! I'm a text-to-speech demonstration. This speech is being generated in real-time as you watch. The words you see highlighted are synchronized with the audio playback, creating a karaoke-style reading experience. You can click to pause or resume, and use the reset button to restart from the beginning. Pretty neat, right?""" + +# Predefined voices from pocket-tts (mapped to HuggingFace files) +# See: https://huggingface.co/kyutai/tts-voices +PREDEFINED_VOICES = { + "alba": "hf://kyutai/tts-voices/alba-mackenna/casual.wav", + "marius": "hf://kyutai/tts-voices/alba-mackenna/merchant.wav", + "javert": "hf://kyutai/tts-voices/alba-mackenna/announcer.wav", + "jean": "hf://kyutai/tts-voices/alba-mackenna/a-moment-by.wav", + "fantine": "hf://kyutai/tts-voices/vctk/p225_023_mic1.wav", + "cosette": "hf://kyutai/tts-voices/vctk/p226_023_mic1.wav", + "eponine": "hf://kyutai/tts-voices/vctk/p227_023_mic1.wav", + "azelma": "hf://kyutai/tts-voices/vctk/p228_023_mic1.wav", +} + +DEFAULT_VOICE = "cosette" + + +@mcp.tool() +def list_voices() -> list[types.TextContent]: + """List available TTS voices. + + Returns the predefined voice names that can be used with the say tool. + You can also use HuggingFace URLs (hf://kyutai/tts-voices/...) or local file paths. + """ + import json + voice_info = { + "predefined_voices": list(PREDEFINED_VOICES.keys()), + "default_voice": DEFAULT_VOICE, + "custom_voice_formats": [ + "hf://kyutai/tts-voices//.wav", + "/path/to/local/voice.wav", + ], + "collections": [ + "alba-mackenna (CC-BY 4.0) - voice-acted characters", + "vctk (CC-BY 4.0) - VCTK dataset speakers", + "cml-tts/fr (CC-BY 4.0) - French voices", + "voice-donations (CC0) - public domain community voices", + "expresso (CC-BY-NC 4.0) - expressive (NON-COMMERCIAL ONLY)", + "ears (CC-BY-NC 4.0) - emotional (NON-COMMERCIAL ONLY)", + ], + } + return [types.TextContent(type="text", text=json.dumps(voice_info, indent=2))] + + +@mcp.tool(meta={ + "ui":{"resourceUri": WIDGET_URI}, + "ui/resourceUri": WIDGET_URI, # legacy support +}) +def say( + text: Annotated[str, Field(description="The English text to speak aloud")] = DEFAULT_TEXT, + voice: Annotated[str, Field( + description="Voice to use. Can be a predefined name (alba, marius, cosette, etc.), " + "a HuggingFace URL (hf://kyutai/tts-voices/...), or a local file path." + )] = DEFAULT_VOICE, + autoPlay: Annotated[bool, Field( + description="Whether to start playing automatically. Note: browsers may block autoplay until user interaction." + )] = True, +) -> list[types.TextContent]: + """Speak English text aloud using text-to-speech. + + Use when the user wants text read or spoken aloud: + - "say ...", "speak ...", "read ... out loud" + - "...; say it", "...; read it to me", "...; speak it" + - "narrate ...", "read this aloud" + + Audio streams in real-time as text is provided. + Use list_voices() for voice options. + + Note: English only. Non-English text may produce poor or garbled results. + """ + # Generate a unique ID for this widget instance (used for speak lock coordination) + widget_uuid = uuid.uuid4().hex[:12] + + # This is a no-op - the widget handles everything via ontoolinputpartial + # The tool exists to: + # 1. Trigger the widget to load + # 2. Provide the resourceUri metadata + # 3. Show the final text in the tool result + # 4. Provide widget UUID for multi-player coordination + return [types.TextContent( + type="text", + text=f"Displayed a TTS widget with voice '{voice}'. Click to play/pause, use toolbar to restart or fullscreen.", + _meta={"widgetUUID": widget_uuid}, + )] + + +# ------------------------------------------------------ +# Private Tools: TTS Queue Management +# ------------------------------------------------------ + +@mcp.tool(meta={"ui":{"visibility":["app"]}}) +async def create_tts_queue(voice: str = "cosette") -> list[types.TextContent]: + """Create a TTS generation queue. Returns queue_id and sample_rate. + + Args: + voice: Voice to use (cosette, alba, brenda, etc.) + """ + if tts_model is None: + return [types.TextContent(type="text", text='{"error": "TTS model not loaded"}')] + + queue_id = uuid.uuid4().hex[:12] + sample_rate = tts_model.config.mimi.sample_rate + + state = TTSQueueState( + id=queue_id, + voice=voice, + sample_rate=sample_rate, + ) + tts_queues[queue_id] = state + + # Start background TTS processing task + state.task = asyncio.create_task(_run_tts_queue(state)) + + logger.info(f"Created TTS queue {queue_id}") + + import json + return [types.TextContent( + type="text", + text=json.dumps({"queue_id": queue_id, "sample_rate": sample_rate}) + )] + + +@mcp.tool(meta={"ui":{"visibility":["app"]}}) +def add_tts_text(queue_id: str, text: str) -> list[types.TextContent]: + """Add text to a TTS queue. + + Args: + queue_id: The queue ID from create_tts_queue + text: Text to add (incremental, not cumulative) + """ + state = tts_queues.get(queue_id) + if not state: + return [types.TextContent(type="text", text='{"error": "Queue not found"}')] + if state.end_signaled: + return [types.TextContent(type="text", text='{"error": "Queue already ended"}')] + + # Queue the text (non-blocking) + try: + state.text_queue.put_nowait(text) + state.last_activity = time.time() # Update activity timestamp + except asyncio.QueueFull: + return [types.TextContent(type="text", text='{"error": "Queue full"}')] + + return [types.TextContent(type="text", text='{"queued": true}')] + + +@mcp.tool(meta={"ui":{"visibility":["app"]}}) +def end_tts_queue(queue_id: str) -> list[types.TextContent]: + """Signal that no more text will be sent to a queue. + + Args: + queue_id: The queue ID from create_tts_queue + """ + state = tts_queues.get(queue_id) + if not state: + logger.warning(f"end_tts_queue called for unknown queue: {queue_id}") + return [types.TextContent(type="text", text='{"error": "Queue not found"}')] + if state.end_signaled: + logger.info(f"end_tts_queue called for already-ended queue: {queue_id}") + return [types.TextContent(type="text", text='{"already_ended": true}')] + + state.end_signaled = True + state.last_activity = time.time() # Update activity timestamp + try: + state.text_queue.put_nowait(None) # EOF marker + except asyncio.QueueFull: + pass + + logger.info(f"end_tts_queue called for queue: {queue_id}") + return [types.TextContent(type="text", text='{"ended": true}')] + + +@mcp.tool(meta={"ui":{"visibility":["app"]}}) +def cancel_tts_queue(queue_id: str) -> list[types.TextContent]: + """Cancel and cleanup a TTS queue. Use before creating a new queue to avoid overlapping playback. + + Args: + queue_id: The queue ID from create_tts_queue + """ + state = tts_queues.pop(queue_id, None) + if not state: + return [types.TextContent(type="text", text='{"error": "Queue not found"}')] + + # Cancel the background task + if state.task and not state.task.done(): + state.task.cancel() + logger.info(f"Cancelled TTS queue {queue_id}") + + # Signal end to unblock any waiting consumers + state.end_signaled = True + try: + state.text_queue.put_nowait(None) + except asyncio.QueueFull: + pass + + state.status = "complete" + + return [types.TextContent(type="text", text='{"cancelled": true}')] + + +@mcp.tool(meta={"ui":{"visibility":["app"]}}) +def poll_tts_audio(queue_id: str) -> list[types.TextContent]: + """Poll for available audio chunks from a TTS queue. + + Returns base64-encoded audio chunks with timing metadata. + Call repeatedly until done=true. + + Args: + queue_id: The queue ID from create_tts_queue + """ + import json + import time + + state = tts_queues.get(queue_id) + if not state: + return [types.TextContent(type="text", text='{"error": "Queue not found"}')] + + # Update last activity to prevent timeout during active polling + state.last_activity = time.time() + + # Get new chunks (use sync approach since we can't await in tool) + # The lock is async, so we need to be careful here + # For simplicity, just grab what's available without locking + new_chunks = state.audio_chunks[state.chunks_delivered:] + state.chunks_delivered = len(state.audio_chunks) + + # Consider queues with errors as "done" so widget stops polling + done = (state.status == "complete" or state.status == "error") and state.chunks_delivered >= len(state.audio_chunks) + + response = { + "chunks": [ + { + "index": c.index, + "audio_base64": c.audio_base64, + "char_start": c.char_start, + "char_end": c.char_end, + "duration_ms": c.duration_ms, + } + for c in new_chunks + ], + "done": done, + "status": state.status, + } + + # Include error message if present + if state.error_message: + response["error"] = state.error_message + + # Clean up completed or errored queues + if done: + # Schedule cleanup after a delay + async def cleanup(): + await asyncio.sleep(60) + tts_queues.pop(queue_id, None) + try: + asyncio.get_event_loop().create_task(cleanup()) + except RuntimeError: + pass + + return [types.TextContent(type="text", text=json.dumps(response))] + + +# ------------------------------------------------------ +# Background TTS Processing +# ------------------------------------------------------ + + +class StreamingTextChunker: + """Buffers streaming text and emits chunks when ready for TTS processing. + + Chunks are emitted when: + - Token count reaches max_tokens threshold (at a sentence boundary if possible) + - flush() is called (end of stream) + + This matches the chunking behavior of split_into_best_sentences() but works + incrementally as text arrives. + """ + + def __init__(self, tokenizer, max_tokens: int = 50, min_tokens: int = 15): + """ + Args: + tokenizer: SentencePiece tokenizer from flow_lm.conditioner.tokenizer + max_tokens: Maximum tokens per chunk (default 50, matches existing) + min_tokens: Minimum tokens before considering emission + """ + self.tokenizer = tokenizer + self.max_tokens = max_tokens + self.min_tokens = min_tokens + self.buffer = "" + + # Cache end-of-sentence token IDs for boundary detection + _, *eos_tokens = tokenizer(".!...?").tokens[0].tolist() + self.eos_tokens = set(eos_tokens) + + def add_text(self, text: str) -> list[str]: + """Add text to buffer, return any complete chunks ready for processing. + + Args: + text: Incremental text to add (e.g., from LLM token) + + Returns: + List of text chunks ready for TTS (may be empty if still buffering) + """ + self.buffer += text + return self._extract_ready_chunks() + + def flush(self) -> list[str]: + """Flush remaining buffer as final chunk(s). + + Call this when the text stream ends to process any remaining text. + + Returns: + List of final text chunks (may be empty if buffer was empty) + """ + if not self.buffer.strip(): + return [] + + # Force emit whatever remains + chunks = self._extract_ready_chunks(force_emit=True) + if self.buffer.strip(): + chunks.append(self.buffer.strip()) + self.buffer = "" + return chunks + + def _extract_ready_chunks(self, force_emit: bool = False) -> list[str]: + """Extract chunks that are ready for processing.""" + chunks = [] + + while True: + chunk = self._try_extract_chunk(force_emit and not chunks) + if chunk is None: + break + chunks.append(chunk) + + return chunks + + def _try_extract_chunk(self, force_emit: bool = False) -> str | None: + """Try to extract one chunk from buffer.""" + text = self.buffer.strip() + if not text: + return None + + tokens = self.tokenizer(text).tokens[0].tolist() + num_tokens = len(tokens) + + # Not enough tokens yet + if num_tokens < self.min_tokens and not force_emit: + return None + + # Under max and not forcing - check for complete sentence worth emitting + if num_tokens < self.max_tokens and not force_emit: + # Only emit early if we have a complete sentence at a good length + if num_tokens >= self.min_tokens and self._ends_with_sentence_boundary(tokens): + # Found a complete sentence - emit it + chunk = text + self.buffer = "" + return chunk + return None + + # Over max_tokens or force_emit - find best split point + split_idx = self._find_best_split(tokens, force_emit) + + if split_idx == 0: + if force_emit: + chunk = text + self.buffer = "" + return chunk + return None + + # Decode tokens up to split point + chunk_text = self.tokenizer.sp.decode(tokens[:split_idx]) + remaining_text = self.tokenizer.sp.decode(tokens[split_idx:]) + + self.buffer = remaining_text + return chunk_text.strip() + + def _find_best_split(self, tokens: list[int], force_emit: bool = False) -> int: + """Find the best token index to split at (sentence boundary near max_tokens).""" + # Find all sentence boundaries (position AFTER the punctuation) + boundaries = [] + prev_was_eos = False + + for i, token in enumerate(tokens): + if token in self.eos_tokens: + prev_was_eos = True + elif prev_was_eos: + boundaries.append(i) + prev_was_eos = False + + # Also consider end of tokens if it ends with punctuation + if tokens and tokens[-1] in self.eos_tokens: + boundaries.append(len(tokens)) + + if not boundaries: + # No sentence boundaries - split at max_tokens if we're over + if len(tokens) >= self.max_tokens: + return self.max_tokens + return len(tokens) if force_emit else 0 + + # Find boundary closest to max_tokens without going too far over + best_boundary = 0 + for boundary in boundaries: + if boundary <= self.max_tokens: + best_boundary = boundary + elif best_boundary == 0: + # First boundary is past max - use it anyway + best_boundary = boundary + break + else: + # We have a good boundary before max, stop + break + + return best_boundary + + def _ends_with_sentence_boundary(self, tokens: list[int]) -> bool: + """Check if token sequence ends with sentence-ending punctuation.""" + if not tokens: + return False + return tokens[-1] in self.eos_tokens + + @property + def buffered_text(self) -> str: + """Current buffered text (for debugging/monitoring).""" + return self.buffer + + @property + def buffered_token_count(self) -> int: + """Approximate token count in buffer.""" + if not self.buffer.strip(): + return 0 + return len(self.tokenizer(self.buffer).tokens[0].tolist()) + + +async def _run_tts_queue(state: TTSQueueState): + """Background task: consume text queue, produce audio chunks.""" + if tts_model is None: + state.status = "error" + state.error_message = "TTS model not loaded" + return + + model_state = tts_model._cached_get_state_for_audio_prompt(state.voice, truncate=True) + chunker = StreamingTextChunker(tts_model.flow_lm.conditioner.tokenizer) + chunk_index = 0 + char_offset = 0 + + try: + while True: + # Wait for text with timeout to detect stale queues + try: + text_item = await asyncio.wait_for( + state.text_queue.get(), + timeout=5.0 # Check every 5 seconds + ) + except asyncio.TimeoutError: + # Check if queue is stale (no activity for too long) + if time.time() - state.last_activity > QUEUE_TIMEOUT_SECONDS: + logger.warning(f"TTS queue {state.id} timeout after {QUEUE_TIMEOUT_SECONDS}s of inactivity") + state.status = "error" + state.error_message = f"Queue timeout: no activity for {QUEUE_TIMEOUT_SECONDS}s" + break + # Continue waiting - queue might still be active + continue + + if text_item is None: + # EOF - flush remaining text + remaining = chunker.flush() + for chunk_text in remaining: + await _process_tts_chunk(state, chunk_text, chunk_index, char_offset, model_state) + char_offset += len(chunk_text) + chunk_index += 1 + + state.status = "complete" + logger.info(f"TTS queue {state.id} complete: {chunk_index} chunks") + break + + # Feed text to chunker + ready_chunks = chunker.add_text(text_item) + + for chunk_text in ready_chunks: + await _process_tts_chunk(state, chunk_text, chunk_index, char_offset, model_state) + char_offset += len(chunk_text) + chunk_index += 1 + + except Exception as e: + logger.error(f"TTS queue {state.id} error: {e}") + state.status = "error" + state.error_message = str(e) + + +async def _process_tts_chunk( + state: TTSQueueState, + text: str, + chunk_index: int, + char_offset: int, + model_state: dict, +): + """Process a text chunk and add audio to state.""" + if tts_model is None: + return + + loop = asyncio.get_event_loop() + audio_bytes_list: list[bytes] = [] + total_samples = 0 + + def generate_sync(): + nonlocal total_samples + _, frames_after_eos = prepare_text_prompt(text) + frames_after_eos += 2 + + for audio_chunk in tts_model._generate_audio_stream_short_text( + model_state=model_state, + text_to_generate=text, + frames_after_eos=frames_after_eos, + copy_state=True, + ): + audio_int16 = (audio_chunk * 32767).to(torch.int16) + audio_bytes_list.append(audio_int16.cpu().numpy().tobytes()) + total_samples += len(audio_chunk) + + await loop.run_in_executor(None, generate_sync) + + combined_audio = b"".join(audio_bytes_list) + duration_ms = (total_samples / state.sample_rate) * 1000 + + chunk_data = AudioChunkData( + index=chunk_index, + audio_base64=base64.b64encode(combined_audio).decode(), + char_start=char_offset, + char_end=char_offset + len(text), + duration_ms=duration_ms, + ) + + async with state.lock: + state.audio_chunks.append(chunk_data) + + logger.debug(f"TTS queue {state.id}: chunk {chunk_index} ready ({duration_ms:.0f}ms)") + + +# ------------------------------------------------------ +# Widget Resource +# ------------------------------------------------------ + +# Embedded widget HTML for standalone execution via `uv run ` +# Uses Babel standalone for in-browser JSX transpilation +# This is a copy of widget.html - keep them in sync! +EMBEDDED_WIDGET_HTML = """ + + + + + Say Widget + + + + + +
+ + +""" + + +def get_widget_html() -> str: + """Get the widget HTML, preferring built version from dist/.""" + # Prefer built version from dist/ (local development with npm run build) + dist_path = Path(__file__).parent / "dist" / "mcp-app.html" + if dist_path.exists(): + return dist_path.read_text() + # Fallback to embedded widget (for `uv run ` or unbundled usage) + return EMBEDDED_WIDGET_HTML + + +# IMPORTANT: all the external domains used by app must be listed +# in the meta.ui.csp.resourceDomains - otherwise they will be blocked by CSP policy +@mcp.resource( + WIDGET_URI, + mime_type="text/html;profile=mcp-app", + meta={"ui": {"csp": {"resourceDomains": ["https://esm.sh", "https://unpkg.com"]}}}, +) +def widget() -> str: + """Widget HTML resource with CSP metadata for external dependencies.""" + return get_widget_html() + + +# ------------------------------------------------------ +# Startup +# ------------------------------------------------------ + +def load_tts_model(): + """Load the TTS model on startup.""" + global tts_model + logger.info("Loading TTS model...") + tts_model = TTSModel.load_model() + logger.info("TTS model loaded") + + +def create_app(): + """Create the ASGI app (for uvicorn reload mode).""" + load_tts_model() + app = mcp.streamable_http_app(stateless_http=True) + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], + ) + return app + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + if "--stdio" in sys.argv: + # Claude Desktop mode + load_tts_model() + mcp.run(transport="stdio") + elif "--reload" in sys.argv: + # Reload mode - pass app as string so uvicorn can reimport + print(f"Say Server listening on http://{HOST}:{PORT}/mcp (reload mode)") + uvicorn.run("server:create_app", host=HOST, port=PORT, reload=True, factory=True) + else: + # HTTP mode + app = create_app() + print(f"Say Server listening on http://{HOST}:{PORT}/mcp") + uvicorn.run(app, host=HOST, port=PORT) diff --git a/package-lock.json b/package-lock.json index 405f314..b82b93a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -617,6 +617,44 @@ "name": "@modelcontextprotocol/server-qr", "version": "1.0.0" }, + "examples/say-server": { + "name": "@modelcontextprotocol/server-say", + "version": "0.4.1", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.4.1", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^4.3.4", + "concurrently": "^9.2.1", + "cross-env": "^10.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "examples/say-server/node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "examples/say-server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", "version": "0.4.1", @@ -2480,6 +2518,10 @@ "resolved": "examples/qr-server", "link": true }, + "node_modules/@modelcontextprotocol/server-say": { + "resolved": "examples/say-server", + "link": true + }, "node_modules/@modelcontextprotocol/server-scenario-modeler": { "resolved": "examples/scenario-modeler-server", "link": true diff --git a/tests/e2e/generate-grid-screenshots.spec.ts b/tests/e2e/generate-grid-screenshots.spec.ts index 80e3c0a..1bc639c 100644 --- a/tests/e2e/generate-grid-screenshots.spec.ts +++ b/tests/e2e/generate-grid-screenshots.spec.ts @@ -58,6 +58,7 @@ const ALL_SERVERS = [ { key: "map-server", name: "Map Server", dir: "map-server" }, { key: "pdf-server", name: "PDF Server", dir: "pdf-server" }, { key: "qr-server", name: "QR Code Server", dir: "qr-server" }, + { key: "say-server", name: "Say Demo", dir: "say-server" }, { key: "scenario-modeler", name: "SaaS Scenario Modeler", diff --git a/tests/e2e/servers.spec.ts b/tests/e2e/servers.spec.ts index a3df89f..d392893 100644 --- a/tests/e2e/servers.spec.ts +++ b/tests/e2e/servers.spec.ts @@ -16,6 +16,7 @@ const DYNAMIC_MASKS: Record = { "basic-vue": ["#server-time"], // Server time display "cohort-heatmap": ['[class*="heatmapWrapper"]'], // Heatmap grid (random data) "customer-segmentation": [".chart-container"], // Scatter plot (random data) + "say-server": [".playBtn", ".playOverlayBtn"], // Play buttons may have different states shadertoy: ["#canvas"], // WebGL shader canvas (animated) "system-monitor": [ ".chart-container", // CPU chart (highly dynamic) diff --git a/tests/e2e/servers.spec.ts-snapshots/say-server.png b/tests/e2e/servers.spec.ts-snapshots/say-server.png new file mode 100644 index 0000000000000000000000000000000000000000..6bd22151ebb97fce6d7d8f976688ead901bb2219 GIT binary patch literal 39655 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