rebrand: rename VoiceBox to TalkBox throughout codebase
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:
2026-08-24 19:45:56 -07:00
parent eaef8dd838
commit b8815e94ea
205 changed files with 1593 additions and 1593 deletions
+22 -22
View File
@@ -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 -1
View File
@@ -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
+8 -8
View File
@@ -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 -1
View File
@@ -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.
"""
+6 -6
View File
@@ -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
View File
@@ -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)