generated from Labyricorn/labyricorn-project-template
rebrand: rename VoiceBox to TalkBox throughout codebase
CI / frontend-quality (push) Canceled after 0s
CI / frontend-quality (push) Canceled after 0s
- All 'voicebox'/'Voicebox'/'VOICEBOX' strings replaced with 'talkbox'/'TalkBox'/'TALKBOX' - Port changed from 17493 to 17494 (avoids conflict with upstream VoiceBox) - MCP tool namespace: voicebox.* -> talkbox.* - App bundle ID: sh.voicebox.app -> com.talkbox.app - Binary names: voicebox-server -> talkbox-server, voicebox-mcp -> talkbox-mcp - Docker user/group: voicebox -> talkbox - Database: voicebox.db -> talkbox.db - Env vars: VOICEBOX_* -> TALKBOX_* - Asset files renamed: voicebox-logo.* -> talkbox-logo.*, etc. - External binaries in tauri.conf.json updated to talkbox-server/talkbox-mcp
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# Voicebox MCP server
|
||||
# TalkBox MCP server
|
||||
|
||||
Local **Model Context Protocol** server — lets any MCP-aware agent
|
||||
(Claude Code, Cursor, Windsurf, VS Code MCP extensions, etc.) speak text
|
||||
in your cloned voices, transcribe audio, and browse captures.
|
||||
|
||||
The server runs inside the same `uvicorn` process as the rest of Voicebox
|
||||
The server runs inside the same `uvicorn` process as the rest of TalkBox
|
||||
and is mounted at `/mcp` (Streamable HTTP transport).
|
||||
|
||||
## Install into your agent
|
||||
@@ -14,23 +14,23 @@ Preferred — direct HTTP:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"voicebox": {
|
||||
"url": "http://127.0.0.1:17493/mcp",
|
||||
"headers": { "X-Voicebox-Client-Id": "claude-code" }
|
||||
"talkbox": {
|
||||
"url": "http://127.0.0.1:17494/mcp",
|
||||
"headers": { "X-TalkBox-Client-Id": "claude-code" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fallback — stdio shim (when the client doesn't speak HTTP MCP). The
|
||||
`voicebox-mcp` binary ships inside the Voicebox.app bundle:
|
||||
`talkbox-mcp` binary ships inside the TalkBox.app bundle:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"voicebox": {
|
||||
"command": "/Applications/Voicebox.app/Contents/MacOS/voicebox-mcp",
|
||||
"env": { "VOICEBOX_CLIENT_ID": "claude-code" }
|
||||
"talkbox": {
|
||||
"command": "/Applications/TalkBox.app/Contents/MacOS/talkbox-mcp",
|
||||
"env": { "TALKBOX_CLIENT_ID": "claude-code" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,25 +39,25 @@ Fallback — stdio shim (when the client doesn't speak HTTP MCP). The
|
||||
Claude Code one-liner:
|
||||
|
||||
```
|
||||
claude mcp add voicebox \
|
||||
claude mcp add talkbox \
|
||||
--transport http \
|
||||
--url http://127.0.0.1:17493/mcp \
|
||||
--header "X-Voicebox-Client-Id: claude-code"
|
||||
--url http://127.0.0.1:17494/mcp \
|
||||
--header "X-TalkBox-Client-Id: claude-code"
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
| Name | Purpose |
|
||||
|---|---|
|
||||
| `voicebox.speak` | Speak text in a voice profile. Returns a generation id you can poll. |
|
||||
| `voicebox.transcribe` | Whisper transcription of a base64 blob or an absolute local path. |
|
||||
| `voicebox.list_captures` | Recent captures (dictation / recording / file) with transcripts. |
|
||||
| `voicebox.list_profiles` | Available voice profiles (cloned + preset). |
|
||||
| `talkbox.speak` | Speak text in a voice profile. Returns a generation id you can poll. |
|
||||
| `talkbox.transcribe` | Whisper transcription of a base64 blob or an absolute local path. |
|
||||
| `talkbox.list_captures` | Recent captures (dictation / recording / file) with transcripts. |
|
||||
| `talkbox.list_profiles` | Available voice profiles (cloned + preset). |
|
||||
|
||||
All tools resolve voice profiles in this precedence:
|
||||
|
||||
1. Explicit `profile` arg (name or id — case-insensitive)
|
||||
2. Per-client binding keyed by `X-Voicebox-Client-Id`
|
||||
2. Per-client binding keyed by `X-TalkBox-Client-Id`
|
||||
3. `capture_settings.default_playback_voice_id` (global default)
|
||||
|
||||
Bindings are managed via `GET|PUT /mcp/bindings` or in the app under
|
||||
@@ -66,11 +66,11 @@ Settings → MCP.
|
||||
## Debug with MCP Inspector
|
||||
|
||||
```
|
||||
npx @modelcontextprotocol/inspector http://127.0.0.1:17493/mcp
|
||||
npx @modelcontextprotocol/inspector http://127.0.0.1:17494/mcp
|
||||
```
|
||||
|
||||
Point it at the URL, hit "List tools," call `voicebox.list_profiles`
|
||||
first to confirm wiring, then `voicebox.speak` for end-to-end.
|
||||
Point it at the URL, hit "List tools," call `talkbox.list_profiles`
|
||||
first to confirm wiring, then `talkbox.speak` for end-to-end.
|
||||
|
||||
## Non-MCP REST surface
|
||||
|
||||
@@ -78,9 +78,9 @@ first to confirm wiring, then `voicebox.speak` for end-to-end.
|
||||
don't speak MCP (shell scripts, ACP, A2A):
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:17493/speak \
|
||||
curl -X POST http://127.0.0.1:17494/speak \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'X-Voicebox-Client-Id: claude-code' \
|
||||
-H 'X-TalkBox-Client-Id: claude-code' \
|
||||
-d '{"text":"Build complete.","profile":"Morgan"}'
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Model Context Protocol server — exposes Voicebox tools to local AI agents.
|
||||
"""Model Context Protocol server — exposes TalkBox tools to local AI agents.
|
||||
|
||||
Mounts a FastMCP instance at /mcp on the main FastAPI app (Streamable HTTP).
|
||||
A bundled stdio shim (backend/mcp_shim) forwards JSON-RPC into the same
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Per-request client identity for MCP calls.
|
||||
|
||||
MCP clients identify themselves via an ``X-Voicebox-Client-Id`` HTTP header
|
||||
MCP clients identify themselves via an ``X-TalkBox-Client-Id`` HTTP header
|
||||
(direct-HTTP clients set it in their MCP config; the stdio shim forwards it
|
||||
from the ``VOICEBOX_CLIENT_ID`` env var). Middleware copies the value into a
|
||||
from the ``TALKBOX_CLIENT_ID`` env var). Middleware copies the value into a
|
||||
ContextVar so tool implementations can read it without plumbing the request
|
||||
object through every service call.
|
||||
"""
|
||||
@@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
# don't get garbage-collected mid-flight (cf. asyncio.create_task docs).
|
||||
_pending_stamps: set[asyncio.Task] = set()
|
||||
|
||||
CLIENT_ID_HEADER = "X-Voicebox-Client-Id"
|
||||
CLIENT_ID_HEADER = "X-TalkBox-Client-Id"
|
||||
|
||||
# Tool handlers read this to apply per-client voice bindings.
|
||||
current_client_id: ContextVar[str | None] = ContextVar(
|
||||
@@ -33,7 +33,7 @@ current_client_id: ContextVar[str | None] = ContextVar(
|
||||
)
|
||||
|
||||
# Remote address of the in-flight request. Used by tools that gate
|
||||
# host-filesystem access to loopback callers (see voicebox.transcribe).
|
||||
# host-filesystem access to loopback callers (see talkbox.transcribe).
|
||||
current_remote_addr: ContextVar[str | None] = ContextVar(
|
||||
"current_remote_addr", default=None
|
||||
)
|
||||
@@ -54,26 +54,26 @@ def request_is_loopback() -> bool:
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# Endpoints that consume X-Voicebox-Client-Id for its MCP-semantic
|
||||
# Endpoints that consume X-TalkBox-Client-Id for its MCP-semantic
|
||||
# meaning (per-client profile resolution + per-client default_personality).
|
||||
# These are the paths where a stamp into last_seen_at is accurate.
|
||||
# Unrelated REST traffic that happens to set the header is intentionally
|
||||
# ignored so the Settings UI's "last heard from" column only reflects
|
||||
# calls that actually acted on the client's bindings.
|
||||
#
|
||||
# - /mcp — FastMCP tool calls (voicebox.speak, voicebox.transcribe, …)
|
||||
# - /mcp — FastMCP tool calls (talkbox.speak, talkbox.transcribe, …)
|
||||
# and the /mcp/bindings admin surface. The admin surface is never
|
||||
# called with the header in practice (the frontend manages bindings
|
||||
# over plain REST), so the `startswith("/mcp")` match doesn't cause
|
||||
# false stamps.
|
||||
# - /speak — REST mirror of voicebox.speak for non-MCP agents (shell
|
||||
# - /speak — REST mirror of talkbox.speak for non-MCP agents (shell
|
||||
# scripts, ACP, A2A). Uses the same per-client binding lookup, so its
|
||||
# callers belong in the last-seen list too.
|
||||
_STAMPED_PATH_PREFIXES: tuple[str, ...] = ("/mcp", "/speak")
|
||||
|
||||
|
||||
class ClientIdMiddleware(BaseHTTPMiddleware):
|
||||
"""Copy X-Voicebox-Client-Id into a ContextVar and stamp last_seen_at
|
||||
"""Copy X-TalkBox-Client-Id into a ContextVar and stamp last_seen_at
|
||||
for requests that act on the caller's MCP bindings."""
|
||||
|
||||
def __init__(self, app: ASGIApp) -> None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""In-memory pub/sub for speaking-pill SSE broadcasts.
|
||||
|
||||
MCP ``voicebox.speak`` calls and the REST ``POST /speak`` route publish
|
||||
MCP ``talkbox.speak`` calls and the REST ``POST /speak`` route publish
|
||||
start/end events that DictateWindow subscribes to via /events/speak, so the
|
||||
floating pill surfaces whenever an agent is speaking.
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The MCP endpoint lives at ``/mcp`` (Streamable HTTP transport). Modern MCP
|
||||
clients (Claude Code, Cursor, Windsurf, VS Code MCP extensions) connect
|
||||
directly via URL; older stdio-only clients use the ``voicebox-mcp`` shim
|
||||
directly via URL; older stdio-only clients use the ``talkbox-mcp`` shim
|
||||
binary bundled with the desktop app.
|
||||
"""
|
||||
|
||||
@@ -23,12 +23,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_mcp_server() -> FastMCP:
|
||||
"""Create the FastMCP instance with Voicebox tools registered."""
|
||||
"""Create the FastMCP instance with TalkBox tools registered."""
|
||||
mcp = FastMCP(
|
||||
name="voicebox",
|
||||
name="talkbox",
|
||||
instructions=(
|
||||
"Voicebox is a local voice I/O layer. Use `voicebox.speak` to "
|
||||
"play text in a voice profile, `voicebox.transcribe` for "
|
||||
"TalkBox is a local voice I/O layer. Use `talkbox.speak` to "
|
||||
"play text in a voice profile, `talkbox.transcribe` for "
|
||||
"audio→text, and the `list_*` tools to discover profiles and "
|
||||
"captures."
|
||||
),
|
||||
@@ -63,7 +63,7 @@ def mount_into(
|
||||
def compose_lifespan(*lifespans):
|
||||
"""Combine multiple async context managers into a single FastAPI lifespan.
|
||||
|
||||
Used by ``create_app`` to run the existing Voicebox startup/shutdown
|
||||
Used by ``create_app`` to run the existing TalkBox startup/shutdown
|
||||
together with FastMCP's session manager (which MUST run in the
|
||||
ASGI lifespan for Streamable HTTP to work).
|
||||
"""
|
||||
|
||||
+17
-17
@@ -1,7 +1,7 @@
|
||||
"""Voicebox MCP tool implementations.
|
||||
"""TalkBox MCP tool implementations.
|
||||
|
||||
Thin wrappers over existing services/routes. Tools are registered with dotted
|
||||
names (``voicebox.speak`` etc.) so they look natural in agent logs —
|
||||
names (``talkbox.speak`` etc.) so they look natural in agent logs —
|
||||
the Python function name stays snake_case.
|
||||
"""
|
||||
|
||||
@@ -33,17 +33,17 @@ MAX_TRANSCRIBE_BYTES = 200 * 1024 * 1024 # 200 MB
|
||||
|
||||
|
||||
def register_tools(mcp: FastMCP) -> None:
|
||||
"""Attach all Voicebox tools to the given FastMCP instance."""
|
||||
"""Attach all TalkBox tools to the given FastMCP instance."""
|
||||
|
||||
@mcp.tool(
|
||||
name="voicebox.speak",
|
||||
name="talkbox.speak",
|
||||
description=(
|
||||
"Speak text in a Voicebox voice profile. Returns a generation id "
|
||||
"Speak text in a TalkBox voice profile. Returns a generation id "
|
||||
"the caller can poll at /generate/{id}/status. Audio plays on the "
|
||||
"user's speakers and is saved to the Captures / History tab."
|
||||
),
|
||||
)
|
||||
async def voicebox_speak(
|
||||
async def talkbox_speak(
|
||||
text: str,
|
||||
profile: str | None = None,
|
||||
engine: str | None = None,
|
||||
@@ -79,7 +79,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
raise ValueError(
|
||||
"No voice profile resolved. Pass `profile=` with a "
|
||||
"voice profile name or id, or set a default voice in "
|
||||
"Voicebox → Settings → MCP."
|
||||
"TalkBox → Settings → MCP."
|
||||
)
|
||||
|
||||
binding = None
|
||||
@@ -113,14 +113,14 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
db.close()
|
||||
|
||||
@mcp.tool(
|
||||
name="voicebox.transcribe",
|
||||
name="talkbox.transcribe",
|
||||
description=(
|
||||
"Transcribe an audio clip to text using Voicebox's local Whisper. "
|
||||
"Transcribe an audio clip to text using TalkBox's local Whisper. "
|
||||
"Pass exactly one of `audio_base64` (bytes as base64) or "
|
||||
"`audio_path` (absolute local file path — loopback callers only)."
|
||||
),
|
||||
)
|
||||
async def voicebox_transcribe(
|
||||
async def talkbox_transcribe(
|
||||
audio_base64: str | None = None,
|
||||
audio_path: str | None = None,
|
||||
language: str | None = None,
|
||||
@@ -132,7 +132,7 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
)
|
||||
|
||||
# Absolute-path mode: validate and transcribe in place. Restricted
|
||||
# to loopback callers so a Voicebox bound on 0.0.0.0 doesn't double
|
||||
# to loopback callers so a TalkBox bound on 0.0.0.0 doesn't double
|
||||
# as an unauthenticated arbitrary-local-file read primitive.
|
||||
if audio_path is not None:
|
||||
if not request_is_loopback():
|
||||
@@ -171,13 +171,13 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
|
||||
@mcp.tool(
|
||||
name="voicebox.list_captures",
|
||||
name="talkbox.list_captures",
|
||||
description=(
|
||||
"List recent voice captures (dictations, recordings, uploads) "
|
||||
"with their transcripts. Most-recent first."
|
||||
),
|
||||
)
|
||||
async def voicebox_list_captures(
|
||||
async def talkbox_list_captures(
|
||||
limit: int = 20, offset: int = 0
|
||||
) -> dict[str, Any]:
|
||||
if not (1 <= limit <= 200):
|
||||
@@ -199,13 +199,13 @@ def register_tools(mcp: FastMCP) -> None:
|
||||
db.close()
|
||||
|
||||
@mcp.tool(
|
||||
name="voicebox.list_profiles",
|
||||
name="talkbox.list_profiles",
|
||||
description=(
|
||||
"List available voice profiles (both cloned voices and presets). "
|
||||
"Use the returned `name` with voicebox.speak(profile=...)."
|
||||
"Use the returned `name` with talkbox.speak(profile=...)."
|
||||
),
|
||||
)
|
||||
async def voicebox_list_profiles() -> dict[str, Any]:
|
||||
async def talkbox_list_profiles() -> dict[str, Any]:
|
||||
db = next(get_db())
|
||||
try:
|
||||
profiles = await profiles_service.list_profiles(db)
|
||||
@@ -318,7 +318,7 @@ async def _transcribe_file(
|
||||
) and not whisper._is_model_cached(model_size):
|
||||
raise ValueError(
|
||||
f"Whisper model '{model_size}' is not yet downloaded. Open "
|
||||
"Voicebox → Settings → Models to download it first."
|
||||
"TalkBox → Settings → Models to download it first."
|
||||
)
|
||||
|
||||
text = await whisper.transcribe(str(path), language, model_size)
|
||||
|
||||
Reference in New Issue
Block a user