Merge remote-tracking branch 'origin/main' into ochafik/readme-mcp-config
This commit is contained in:
commit
5ef3120e0c
34 changed files with 2711 additions and 217 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
|
|
@ -24,11 +25,8 @@ export interface ServerInfo {
|
|||
|
||||
|
||||
export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
|
||||
const client = new Client(IMPLEMENTATION);
|
||||
|
||||
log.info("Connecting to server:", serverUrl.href);
|
||||
await client.connect(new StreamableHTTPClientTransport(serverUrl));
|
||||
log.info("Connection successful");
|
||||
const client = await connectWithFallback(serverUrl);
|
||||
|
||||
const name = client.getServerVersion()?.name ?? serverUrl.href;
|
||||
|
||||
|
|
@ -39,6 +37,28 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
|
|||
return { name, client, tools, appHtmlCache: new Map() };
|
||||
}
|
||||
|
||||
async function connectWithFallback(serverUrl: URL): Promise<Client> {
|
||||
// Try Streamable HTTP first (modern transport)
|
||||
try {
|
||||
const client = new Client(IMPLEMENTATION);
|
||||
await client.connect(new StreamableHTTPClientTransport(serverUrl));
|
||||
log.info("Connected via Streamable HTTP transport");
|
||||
return client;
|
||||
} catch (streamableError) {
|
||||
log.info("Streamable HTTP failed:", streamableError);
|
||||
}
|
||||
|
||||
// Fall back to SSE (deprecated but needed for older servers)
|
||||
try {
|
||||
const client = new Client(IMPLEMENTATION);
|
||||
await client.connect(new SSEClientTransport(serverUrl));
|
||||
log.info("Connected via SSE transport");
|
||||
return client;
|
||||
} catch (sseError) {
|
||||
throw new Error(`Could not connect with any transport. SSE error: ${sseError}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface UiResourceData {
|
||||
html: string;
|
||||
|
|
|
|||
|
|
@ -10,3 +10,9 @@ html, body {
|
|||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Server time fills remaining width for consistent E2E screenshot masking */
|
||||
#server-time {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Server time row: flex layout for consistent mask width in E2E tests */
|
||||
> p {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
/* Consistent font for form inputs (inherits from global.css) */
|
||||
textarea,
|
||||
input {
|
||||
|
|
|
|||
|
|
@ -10,3 +10,9 @@ html, body {
|
|||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Server time fills remaining width for consistent E2E screenshot masking */
|
||||
#server-time {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Server time row: flex layout for consistent mask width in E2E tests */
|
||||
> p {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
/* Consistent font for form inputs (inherits from global.css) */
|
||||
textarea,
|
||||
input {
|
||||
|
|
|
|||
|
|
@ -10,3 +10,9 @@ html, body {
|
|||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Server time fills remaining width for consistent E2E screenshot masking */
|
||||
#server-time {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Server time row: flex layout for consistent mask width in E2E tests */
|
||||
> p {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
/* Consistent font for form inputs (inherits from global.css) */
|
||||
textarea,
|
||||
input {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,13 @@ async function handleOpenLink() {
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Server time row: flex layout for consistent mask width in E2E tests */
|
||||
> p {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input {
|
||||
font-family: inherit;
|
||||
|
|
@ -196,4 +203,10 @@ async function handleOpenLink() {
|
|||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* Server time fills remaining width for consistent E2E screenshot masking */
|
||||
:global(#server-time) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,3 +10,9 @@ html, body {
|
|||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Server time fills remaining width for consistent E2E screenshot masking */
|
||||
#server-time {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Server time row: flex layout for consistent mask width in E2E tests */
|
||||
> p {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
/* Consistent font for form inputs (inherits from global.css) */
|
||||
textarea,
|
||||
input {
|
||||
|
|
|
|||
|
|
@ -170,6 +170,13 @@ async function handleOpenLink() {
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Server time row: flex layout for consistent mask width in E2E tests */
|
||||
> p {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input {
|
||||
font-family: inherit;
|
||||
|
|
@ -204,4 +211,10 @@ async function handleOpenLink() {
|
|||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* Server time fills remaining width for consistent E2E screenshot masking */
|
||||
:deep(#server-time) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,3 +10,9 @@ html, body {
|
|||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Server time fills remaining width for consistent E2E screenshot masking */
|
||||
#server-time {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Server time row: flex layout for consistent mask width in E2E tests */
|
||||
> p {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
/* Consistent font for form inputs (inherits from global.css) */
|
||||
textarea,
|
||||
input {
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 27 KiB |
|
|
@ -2,7 +2,7 @@
|
|||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "mcp>=1.9.0",
|
||||
# "mcp @ git+https://github.com/modelcontextprotocol/python-sdk@main",
|
||||
# "qrcode[pil]>=8.0",
|
||||
# "uvicorn>=0.34.0",
|
||||
# "starlette>=0.46.0",
|
||||
|
|
@ -30,7 +30,10 @@ PORT = int(os.environ.get("PORT", "3108"))
|
|||
mcp = FastMCP("QR Code Server", port=PORT, stateless_http=True)
|
||||
|
||||
|
||||
@mcp.tool(meta={"ui/resourceUri": WIDGET_URI})
|
||||
@mcp.tool(meta={
|
||||
"ui":{"resourceUri": WIDGET_URI},
|
||||
"ui/resourceUri": WIDGET_URI, # legacy support
|
||||
})
|
||||
def generate_qr(
|
||||
text: str = "https://modelcontextprotocol.io",
|
||||
box_size: int = 10,
|
||||
|
|
@ -72,53 +75,17 @@ def generate_qr(
|
|||
return [types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
||||
|
||||
|
||||
# Register widget resource using FastMCP decorator (returns HTML string)
|
||||
@mcp.resource(WIDGET_URI, mime_type="text/html;profile=mcp-app")
|
||||
# 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://unpkg.com"]}}},
|
||||
)
|
||||
def widget() -> str:
|
||||
"""Widget HTML resource with CSP metadata for external dependencies."""
|
||||
return Path(__file__).parent.joinpath("widget.html").read_text()
|
||||
|
||||
|
||||
# Override the read_resource handler to inject _meta into the response
|
||||
# This is needed because FastMCP doesn't support custom _meta on resources
|
||||
_low_level_server = mcp._mcp_server
|
||||
|
||||
|
||||
async def _read_resource_with_meta(req: types.ReadResourceRequest):
|
||||
"""Custom handler that injects CSP metadata for the widget resource."""
|
||||
uri = str(req.params.uri)
|
||||
html = Path(__file__).parent.joinpath("widget.html").read_text()
|
||||
|
||||
if uri == WIDGET_URI:
|
||||
# NOTE: Must use model_validate with '_meta' key (not 'meta') due to Pydantic alias behavior
|
||||
content = types.TextResourceContents.model_validate({
|
||||
"uri": WIDGET_URI,
|
||||
"mimeType": "text/html;profile=mcp-app",
|
||||
"text": 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
|
||||
"_meta": {"ui": {"csp": {"resourceDomains": ["https://unpkg.com"]}}}
|
||||
})
|
||||
return types.ServerResult(
|
||||
types.ReadResourceResult(contents=[content])
|
||||
)
|
||||
|
||||
# Fallback for other resources (shouldn't happen for this server)
|
||||
return types.ServerResult(
|
||||
types.ReadResourceResult(
|
||||
contents=[
|
||||
types.TextResourceContents(
|
||||
uri=uri,
|
||||
mimeType="text/plain",
|
||||
text="Resource not found"
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Replace the handler after FastMCP has registered its own
|
||||
_low_level_server.request_handlers[types.ReadResourceRequest] = _read_resource_with_meta
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--stdio" in sys.argv:
|
||||
# Claude Desktop mode
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
* bun examples/run-all.ts start - Build and start all examples
|
||||
* bun examples/run-all.ts dev - Run all examples in dev/watch mode
|
||||
* bun examples/run-all.ts build - Build all examples
|
||||
*
|
||||
* Environment:
|
||||
* EXAMPLE=<folder> - Run only a single example (e.g., EXAMPLE=say-server)
|
||||
*/
|
||||
|
||||
import { readdirSync, statSync, existsSync } from "fs";
|
||||
|
|
@ -14,21 +17,36 @@ import concurrently from "concurrently";
|
|||
const BASE_PORT = 3101;
|
||||
const BASIC_HOST = "basic-host";
|
||||
|
||||
// Optional: filter to a single example via EXAMPLE env var (folder name)
|
||||
const EXAMPLE_FILTER = process.env.EXAMPLE;
|
||||
|
||||
// Find all example directories except basic-host that have a package.json,
|
||||
// assign ports, and build URL list
|
||||
const servers = readdirSync("examples")
|
||||
const allServers = readdirSync("examples")
|
||||
.filter(
|
||||
(d) =>
|
||||
d !== BASIC_HOST &&
|
||||
statSync(`examples/${d}`).isDirectory() &&
|
||||
existsSync(`examples/${d}/package.json`),
|
||||
)
|
||||
.sort() // Sort for consistent port assignment
|
||||
.map((dir, i) => ({
|
||||
dir,
|
||||
port: BASE_PORT + i,
|
||||
url: `http://localhost:${BASE_PORT + i}/mcp`,
|
||||
}));
|
||||
.sort(); // Sort for consistent port assignment
|
||||
|
||||
// Filter servers if EXAMPLE is specified
|
||||
const filteredDirs = EXAMPLE_FILTER
|
||||
? allServers.filter((d) => d === EXAMPLE_FILTER)
|
||||
: allServers;
|
||||
|
||||
if (EXAMPLE_FILTER && filteredDirs.length === 0) {
|
||||
console.error(`Error: No example found matching EXAMPLE=${EXAMPLE_FILTER}`);
|
||||
console.error(`Available examples: ${allServers.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const servers = filteredDirs.map((dir, i) => ({
|
||||
dir,
|
||||
port: BASE_PORT + i,
|
||||
url: `http://localhost:${BASE_PORT + i}/mcp`,
|
||||
}));
|
||||
|
||||
const COMMANDS = ["start", "dev", "build"];
|
||||
|
||||
|
|
@ -43,6 +61,9 @@ if (!command || !COMMANDS.includes(command)) {
|
|||
const serversEnv = JSON.stringify(servers.map((s) => s.url));
|
||||
|
||||
console.log(`Running command: ${command}`);
|
||||
if (EXAMPLE_FILTER) {
|
||||
console.log(`Filtering to single example: ${EXAMPLE_FILTER}`);
|
||||
}
|
||||
console.log(
|
||||
`Server examples: ${servers.map((s) => `${s.dir}:${s.port}`).join(", ")}`,
|
||||
);
|
||||
|
|
|
|||
2
examples/say-server/.gitignore
vendored
Normal file
2
examples/say-server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
||||
175
examples/say-server/README.md
Normal file
175
examples/say-server/README.md
Normal file
|
|
@ -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.
|
||||
BIN
examples/say-server/grid-cell.png
Normal file
BIN
examples/say-server/grid-cell.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
14
examples/say-server/mcp-app.html
Normal file
14
examples/say-server/mcp-app.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Say Widget</title>
|
||||
<link rel="stylesheet" href="/src/global.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/mcp-app.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue