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
+8 -8
View File
@@ -1,4 +1,4 @@
# Voicebox Backend
# TalkBox Backend
FastAPI server powering voice cloning, speech generation, and audio processing. Runs locally as a Tauri sidecar or standalone via `python -m backend.main`.
@@ -9,7 +9,7 @@ FastAPI server powering voice cloning, speech generation, and audio processing.
just dev:server
# Standalone
python -m backend.main --host 127.0.0.1 --port 17493
python -m backend.main --host 127.0.0.1 --port 17494
# With custom data directory
python -m backend.main --data-dir /path/to/data
@@ -75,7 +75,7 @@ Detection is handled by `utils/platform_detect.py`. Both backends implement the
## API
90 endpoints organized by domain. Full interactive documentation available at `http://localhost:17493/docs` when the server is running.
90 endpoints organized by domain. Full interactive documentation available at `http://localhost:17494/docs` when the server is running.
| Domain | Prefix | Description |
|--------|--------|-------------|
@@ -96,29 +96,29 @@ Detection is handled by `utils/platform_detect.py`. Both backends implement the
```bash
# Generate speech
curl -X POST http://localhost:17493/generate \
curl -X POST http://localhost:17494/generate \
-H "Content-Type: application/json" \
-d '{"text": "Hello world", "profile_id": "...", "language": "en"}'
# List profiles
curl http://localhost:17493/profiles
curl http://localhost:17494/profiles
# Stream generation status (SSE)
curl http://localhost:17493/generate/{id}/status
curl http://localhost:17494/generate/{id}/status
```
## Data directory
```
{data_dir}/
voicebox.db # SQLite database
talkbox.db # SQLite database
profiles/{id}/ # Voice samples per profile
generations/ # Generated audio files
cache/ # Voice prompt cache (memory + disk)
backends/ # Downloaded CUDA binary (if applicable)
```
Default location is the OS-specific app data directory. Override with `--data-dir` or the `VOICEBOX_DATA_DIR` environment variable.
Default location is the OS-specific app data directory. Override with `--data-dir` or the `TALKBOX_DATA_DIR` environment variable.
## Code quality
+9 -9
View File
@@ -139,7 +139,7 @@ def create_app() -> FastAPI:
mcp_app = mcp.http_app(path="/", transport="http")
@asynccontextmanager
async def voicebox_lifespan(app: FastAPI):
async def talkbox_lifespan(app: FastAPI):
await _run_startup(app)
try:
yield
@@ -149,16 +149,16 @@ def create_app() -> FastAPI:
# startup still unloads whatever models were loaded.
await _run_shutdown()
# compose_lifespan enters factories in order (voicebox startup →
# compose_lifespan enters factories in order (talkbox startup →
# MCP startup) and exits in LIFO (MCP teardown first → models
# unload last). That ordering matters on shutdown: FastMCP's
# __aexit__ cancels in-flight session tasks, and we want that to
# happen *before* _run_shutdown yanks the TTS / Whisper / LLM
# models out from under any MCP request that was still generating.
lifespan = compose_lifespan(voicebox_lifespan, mcp_app.router.lifespan_context)
lifespan = compose_lifespan(talkbox_lifespan, mcp_app.router.lifespan_context)
application = FastAPI(
title="voicebox API",
title="talkbox API",
description="Production-quality Qwen3-TTS voice cloning API",
version=__version__,
lifespan=lifespan,
@@ -179,13 +179,13 @@ def _configure_cors(application: FastAPI) -> None:
default_origins = [
"http://localhost:5173", # Vite dev server
"http://127.0.0.1:5173",
"http://localhost:17493",
"http://127.0.0.1:17493",
"http://localhost:17494",
"http://127.0.0.1:17494",
"tauri://localhost", # Tauri webview (macOS)
"https://tauri.localhost", # Tauri webview (Windows/Linux)
"http://tauri.localhost", # Tauri webview (Windows, some builds)
]
env_origins = os.environ.get("VOICEBOX_CORS_ORIGINS", "")
env_origins = os.environ.get("TALKBOX_CORS_ORIGINS", "")
all_origins = default_origins + [o.strip() for o in env_origins.split(",") if o.strip()]
application.add_middleware(
@@ -276,7 +276,7 @@ async def _run_startup(application: FastAPI) -> None:
import platform
import sys
logger.info("Voicebox v%s starting up", __version__)
logger.info("TalkBox v%s starting up", __version__)
logger.info(
"Python %s on %s %s (%s)",
sys.version.split()[0],
@@ -358,7 +358,7 @@ async def _run_startup(application: FastAPI) -> None:
async def _run_shutdown() -> None:
"""Unload models on lifespan exit."""
logger.info("Voicebox server shutting down...")
logger.info("TalkBox server shutting down...")
try:
tts.unload_tts_model()
except Exception:
+14 -14
View File
@@ -27,9 +27,9 @@ def build_server(cuda=False, rocm=False):
Args:
cuda: If True, build with CUDA support and name the binary
voicebox-server-cuda instead of voicebox-server.
talkbox-server-cuda instead of talkbox-server.
rocm: If True, build with ROCm support and name the binary
voicebox-server-rocm instead of voicebox-server.
talkbox-server-rocm instead of talkbox-server.
"""
if cuda and rocm:
raise ValueError("Cannot build with both CUDA and ROCm support")
@@ -37,11 +37,11 @@ def build_server(cuda=False, rocm=False):
backend_dir = Path(__file__).parent
if rocm:
binary_name = "voicebox-server-rocm"
binary_name = "talkbox-server-rocm"
elif cuda:
binary_name = "voicebox-server-cuda"
binary_name = "talkbox-server-cuda"
else:
binary_name = "voicebox-server"
binary_name = "talkbox-server"
# PyInstaller arguments
# CUDA and ROCm builds use --onedir so we can split the output into two archives:
@@ -305,7 +305,7 @@ def build_server(cuda=False, rocm=False):
"unidic_lite",
"--hidden-import",
"loguru",
# MCP server — Streamable-HTTP endpoint and the 4 voicebox.* tools.
# MCP server — Streamable-HTTP endpoint and the 4 talkbox.* tools.
# FastMCP pulls in a chain of deps (mcp, cyclopts, openapi-pydantic,
# etc.) that don't auto-discover cleanly under PyInstaller, so we
# collect them whole. Small compared to torch.
@@ -673,10 +673,10 @@ def build_server(cuda=False, rocm=False):
def build_shim():
"""Build the voicebox-mcp stdio shim as a tiny standalone binary.
"""Build the talkbox-mcp stdio shim as a tiny standalone binary.
This is the bridge for MCP clients that only speak stdio — it proxies
JSON-RPC to the main voicebox-server's /mcp endpoint. Keep it small: no
JSON-RPC to the main talkbox-server's /mcp endpoint. Keep it small: no
torch, no ML deps, just httpx + asyncio.
"""
backend_dir = Path(__file__).parent
@@ -685,7 +685,7 @@ def build_shim():
"mcp_shim/__main__.py",
"--onefile",
"--name",
"voicebox-mcp",
"talkbox-mcp",
# Stdio-only — no console hiding needed on Windows since the parent
# MCP client is spawning this as a child process and wants stdio.
"--hidden-import",
@@ -759,25 +759,25 @@ def build_shim():
os.chdir(backend_dir)
PyInstaller.__main__.run(args)
logger.info("Shim built: %s", backend_dir / "dist" / "voicebox-mcp")
logger.info("Shim built: %s", backend_dir / "dist" / "talkbox-mcp")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Build voicebox binaries")
parser = argparse.ArgumentParser(description="Build talkbox binaries")
parser.add_argument(
"--cuda",
action="store_true",
help="Build CUDA-enabled binary (voicebox-server-cuda)",
help="Build CUDA-enabled binary (talkbox-server-cuda)",
)
parser.add_argument(
"--rocm",
action="store_true",
help="Build ROCm-enabled binary (voicebox-server-rocm) for AMD GPUs",
help="Build ROCm-enabled binary (talkbox-server-rocm) for AMD GPUs",
)
parser.add_argument(
"--shim",
action="store_true",
help="Build the voicebox-mcp stdio shim binary instead of the server",
help="Build the talkbox-mcp stdio shim binary instead of the server",
)
cli_args = parser.parse_args()
if cli_args.shim:
+12 -12
View File
@@ -1,5 +1,5 @@
"""
Configuration module for voicebox backend.
Configuration module for talkbox backend.
Handles data directory configuration for production bundling.
"""
@@ -11,9 +11,9 @@ from pathlib import Path
logger = logging.getLogger(__name__)
# Allow users to override the HuggingFace model download directory.
# Set VOICEBOX_MODELS_DIR to an absolute path before starting the server.
# Set TALKBOX_MODELS_DIR to an absolute path before starting the server.
# This sets HF_HUB_CACHE so all huggingface_hub downloads go to that path.
_custom_models_dir = os.environ.get("VOICEBOX_MODELS_DIR")
_custom_models_dir = os.environ.get("TALKBOX_MODELS_DIR")
if _custom_models_dir:
os.environ["HF_HUB_CACHE"] = _custom_models_dir
logger.info("Model download path set to: %s", _custom_models_dir)
@@ -107,7 +107,7 @@ def resolve_storage_path(path: str | Path | None) -> Path | None:
def get_db_path() -> Path:
"""Get database file path."""
return _data_dir / "voicebox.db"
return _data_dir / "talkbox.db"
def get_profiles_dir() -> Path:
@@ -145,15 +145,15 @@ def get_models_dir() -> Path:
return path
# Voicebox Cloud (backup & sync). Two hosts: the web app owns auth + device
# pairing (voicebox.sh), the API owns sync + account endpoints
# (api.voicebox.sh). Override both for local development, e.g.
# VOICEBOX_CLOUD_URL=http://localhost:17592 VOICEBOX_CLOUD_API_URL=http://localhost:17593
# TalkBox Cloud (backup & sync). Two hosts: the web app owns auth + device
# pairing (talkbox.sh), the API owns sync + account endpoints
# (api.talkbox.sh). Override both for local development, e.g.
# TALKBOX_CLOUD_URL=http://localhost:17592 TALKBOX_CLOUD_API_URL=http://localhost:17593
def get_cloud_web_url() -> str:
"""Base URL of the Voicebox Cloud web app (auth + /connect + exchange)."""
return os.environ.get("VOICEBOX_CLOUD_URL", "https://voicebox.sh").rstrip("/")
"""Base URL of the TalkBox Cloud web app (auth + /connect + exchange)."""
return os.environ.get("TALKBOX_CLOUD_URL", "https://talkbox.sh").rstrip("/")
def get_cloud_api_url() -> str:
"""Base URL of the Voicebox Cloud API (bearer-authenticated sync/account)."""
return os.environ.get("VOICEBOX_CLOUD_API_URL", "https://api.voicebox.sh").rstrip("/")
"""Base URL of the TalkBox Cloud API (bearer-authenticated sync/account)."""
return os.environ.get("TALKBOX_CLOUD_API_URL", "https://api.talkbox.sh").rstrip("/")
+3 -3
View File
@@ -1,6 +1,6 @@
"""Column-level migrations for the voicebox SQLite database.
"""Column-level migrations for the talkbox SQLite database.
Why not Alembic? voicebox is a single-user desktop app shipping as a
Why not Alembic? talkbox is a single-user desktop app shipping as a
PyInstaller binary. Every user has exactly one SQLite file. Alembic's
strengths -- migration tracking across environments, rollback, team
coordination -- don't apply here and would add bundling complexity
@@ -249,7 +249,7 @@ def _migrate_mcp_bindings(engine, inspector, tables: set[str]) -> None:
"""Drop the legacy ``default_intent`` column and add ``default_personality``.
The intent tri-state (respond / rewrite / compose) has been collapsed
to a boolean: when true, ``voicebox.speak`` rewrites input through the
to a boolean: when true, ``talkbox.speak`` rewrites input through the
profile's personality LLM before TTS.
"""
if "mcp_client_bindings" not in tables:
+7 -7
View File
@@ -1,4 +1,4 @@
"""ORM model definitions for the voicebox SQLite database."""
"""ORM model definitions for the talkbox SQLite database."""
from datetime import datetime
import uuid
@@ -207,7 +207,7 @@ class CaptureSettings(Base):
# Default OFF — opting in is what triggers the macOS Input Monitoring TCC
# prompt. We deliberately don't spawn the global keyboard tap until the
# user flips this on so a fresh-install user doesn't see a scary
# "Voicebox would like to receive keystrokes from any application" dialog
# "TalkBox would like to receive keystrokes from any application" dialog
# before they've even opened the Captures tab.
hotkey_enabled = Column(Boolean, nullable=False, default=False)
# Lists of keytap key names (e.g. "MetaRight", "ControlRight"). Right-hand
@@ -235,12 +235,12 @@ class GenerationSettings(Base):
class CloudSettings(Base):
"""Singleton row holding the link to a Voicebox Cloud account.
"""Singleton row holding the link to a TalkBox Cloud account.
Populated by the "Log in with browser" pairing flow (see services/cloud.py):
the browser hands back a one-time code, which the backend exchanges for an
``api_key`` it stores here. The key is a bearer credential for
api.voicebox.sh — auth only, never an encryption key (E2E key material lives
api.talkbox.sh — auth only, never an encryption key (E2E key material lives
elsewhere). Stored in the local app database alongside the user's other data;
moving it to the OS keychain is a future hardening step. The ``id`` is
always 1; a null ``api_key`` means "not connected".
@@ -261,9 +261,9 @@ class MCPClientBinding(Base):
Lets users bind distinct voices to distinct agents — e.g. Claude Code
speaks in "Morgan," Cursor in "Scarlett." The MCP client identifies
itself via the ``X-Voicebox-Client-Id`` HTTP header; direct-HTTP
itself via the ``X-TalkBox-Client-Id`` HTTP header; direct-HTTP
clients set it in their MCP config's ``headers`` block, the stdio
shim forwards it from the ``VOICEBOX_CLIENT_ID`` env var.
shim forwards it from the ``TALKBOX_CLIENT_ID`` env var.
"""
__tablename__ = "mcp_client_bindings"
@@ -272,7 +272,7 @@ class MCPClientBinding(Base):
label = Column(String, nullable=True) # display name
profile_id = Column(String, ForeignKey("profiles.id"), nullable=True)
default_engine = Column(String, nullable=True)
# When true, voicebox.speak routes through the profile's personality LLM
# When true, talkbox.speak routes through the profile's personality LLM
# (rewrite) before TTS by default. Callers can still override per call.
default_personality = Column(Boolean, nullable=False, default=False)
last_seen_at = Column(DateTime, nullable=True)
+2 -2
View File
@@ -1,4 +1,4 @@
"""Entry point for the voicebox backend.
"""Entry point for the talkbox backend.
Imports the configured FastAPI app and provides a ``python -m backend.main``
entry point for development.
@@ -11,7 +11,7 @@ from .app import app # noqa: F401 -- re-export for uvicorn "backend.main:app"
from . import config, database
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="voicebox backend server")
parser = argparse.ArgumentParser(description="talkbox backend server")
parser.add_argument(
"--host",
type=str,
+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)
+4 -4
View File
@@ -1,10 +1,10 @@
"""Stdio → Streamable HTTP bridge for the Voicebox MCP server.
"""Stdio → Streamable HTTP bridge for the TalkBox MCP server.
Some MCP clients only know how to spawn a subprocess and talk to it over
stdin/stdout (the "stdio" transport). This package is a ~150-line adapter:
the client spawns us as ``voicebox-mcp``; we proxy every JSON-RPC frame
to http://127.0.0.1:17493/mcp/ and stream responses back out.
the client spawns us as ``talkbox-mcp``; we proxy every JSON-RPC frame
to http://127.0.0.1:17494/mcp/ and stream responses back out.
All the real work (tools, models, inference) lives in the Voicebox server
All the real work (tools, models, inference) lives in the TalkBox server
process this package contains no business logic.
"""
+13 -13
View File
@@ -1,13 +1,13 @@
"""voicebox-mcp — stdio ↔ Streamable-HTTP MCP proxy.
"""talkbox-mcp — stdio ↔ Streamable-HTTP MCP proxy.
Some MCP clients only speak stdio. They spawn this binary, we pipe each
JSON-RPC message to ``http://127.0.0.1:<port>/mcp/``, and stream the
server's response back. The Voicebox server does all the real work.
server's response back. The TalkBox server does all the real work.
Environment variables:
VOICEBOX_PORT Voicebox server port (default 17493).
VOICEBOX_HOST Host (default 127.0.0.1).
VOICEBOX_CLIENT_ID Forwarded as X-Voicebox-Client-Id on every request.
TALKBOX_PORT TalkBox server port (default 17494).
TALKBOX_HOST Host (default 127.0.0.1).
TALKBOX_CLIENT_ID Forwarded as X-TalkBox-Client-Id on every request.
Stdout is JSON-RPC only. Diagnostics go to stderr.
Exit 0 on clean EOF, 1 on transport error, 2 if backend never answers.
@@ -24,19 +24,19 @@ from typing import Any
import httpx
CLIENT_ID_HEADER = "X-Voicebox-Client-Id"
CLIENT_ID_HEADER = "X-TalkBox-Client-Id"
SESSION_HEADER = "mcp-session-id"
HEALTH_TIMEOUT_S = 30.0
DEFAULT_PORT = 17493
DEFAULT_PORT = 17494
def _err(msg: str) -> None:
print(f"voicebox-mcp: {msg}", file=sys.stderr, flush=True)
print(f"talkbox-mcp: {msg}", file=sys.stderr, flush=True)
def _base_url() -> tuple[str, str]:
host = os.environ.get("VOICEBOX_HOST", "127.0.0.1")
port = int(os.environ.get("VOICEBOX_PORT", str(DEFAULT_PORT)))
host = os.environ.get("TALKBOX_HOST", "127.0.0.1")
port = int(os.environ.get("TALKBOX_PORT", str(DEFAULT_PORT)))
return f"http://{host}:{port}/mcp/", f"http://{host}:{port}/health"
@@ -122,7 +122,7 @@ async def _handle_request(
"error": {
"code": -32000,
"message": (
f"Voicebox MCP proxy got HTTP {response.status_code}"
f"TalkBox MCP proxy got HTTP {response.status_code}"
),
},
}
@@ -155,7 +155,7 @@ async def _handle_request(
async def _run() -> int:
url, health_url = _base_url()
forward_headers: dict[str, str] = {}
client_id = os.environ.get("VOICEBOX_CLIENT_ID")
client_id = os.environ.get("TALKBOX_CLIENT_ID")
if client_id:
forward_headers[CLIENT_ID_HEADER] = client_id
@@ -164,7 +164,7 @@ async def _run() -> int:
async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client:
if not await _wait_for_backend(client, health_url):
_err(
f"timed out waiting for Voicebox at {health_url} — is the app open?"
f"timed out waiting for TalkBox at {health_url} — is the app open?"
)
return 2
+3 -3
View File
@@ -309,7 +309,7 @@ class GenerationSettingsUpdate(BaseModel):
class MCPClientBindingResponse(BaseModel):
"""Per-MCP-client voice binding — what voice / engine the server should
use when a given client_id calls voicebox.speak without args, plus an
use when a given client_id calls talkbox.speak without args, plus an
opt-in personality-rewrite default."""
client_id: str
@@ -346,7 +346,7 @@ class MCPClientBindingListResponse(BaseModel):
class SpeakRequest(BaseModel):
"""Body for POST /speak — non-MCP REST surface that mirrors voicebox.speak."""
"""Body for POST /speak — non-MCP REST surface that mirrors talkbox.speak."""
text: str = Field(..., min_length=1, max_length=10000)
profile: Optional[str] = Field(
@@ -807,7 +807,7 @@ class CloudLoginStartResponse(BaseModel):
class CloudStatusResponse(BaseModel):
"""Current link between this device and a Voicebox Cloud account."""
"""Current link between this device and a TalkBox Cloud account."""
connected: bool
device_name: Optional[str] = None
+3 -3
View File
@@ -32,7 +32,7 @@ trips the decorator chain.
Fix
---
voicebox never uses torch.compile / torch._dynamo for inference, so we
talkbox never uses torch.compile / torch._dynamo for inference, so we
replace torch._dynamo with a no-op stub module before transformers is
imported. Any attribute access on the stub returns a pass-through callable,
so `@torch._dynamo.allow_in_graph`, `torch._dynamo.is_compiling()`,
@@ -60,7 +60,7 @@ import types
# Diagnostics — log hook activity to a file alongside the bundle so we can
# see what's happening when the server is run as a sidecar (no stdout for
# runtime hook prints). Safe no-op if the file can't be written.
_DIAG_PATH = os.path.join(tempfile.gettempdir(), "voicebox_rt_hook.log")
_DIAG_PATH = os.path.join(tempfile.gettempdir(), "talkbox_rt_hook.log")
def _diag(msg: str) -> None:
@@ -193,7 +193,7 @@ class _TransformersStubFinder:
unbound before `del obj`.
The exports (AssistedCandidateGenerator, EarlyExitCandidateGenerator,
etc.) are speculative-decoding helpers voicebox's TTS engines do not
etc.) are speculative-decoding helpers talkbox's TTS engines do not
use; a no-op stub module satisfies the imports.
"""
+2 -2
View File
@@ -1,5 +1,5 @@
[project]
name = "voicebox-backend"
name = "talkbox-backend"
version = "0.2.3"
requires-python = ">=3.12"
@@ -14,7 +14,7 @@ src = ["."]
# Files/dirs to skip entirely.
extend-exclude = [
"voicebox-server.spec",
"talkbox-server.spec",
"build_binary.py",
]
+1 -1
View File
@@ -64,7 +64,7 @@ pedalboard>=0.9.0
httpx>=0.27.0
# MCP server (Model Context Protocol) — lets local AI agents call
# voicebox.speak / .transcribe / .list_captures / .list_profiles
# talkbox.speak / .transcribe / .list_captures / .list_profiles
fastmcp>=3.0,<4.0
sse-starlette>=2.0
+1 -1
View File
@@ -1,4 +1,4 @@
"""Route registration for the voicebox API."""
"""Route registration for the talkbox API."""
from fastapi import FastAPI
+4 -4
View File
@@ -1,4 +1,4 @@
"""Voicebox Cloud device login routes.
"""TalkBox Cloud device login routes.
The browser-based pairing flow:
1. POST /cloud/login/start opens the browser to the cloud authorize page.
@@ -23,7 +23,7 @@ router = APIRouter(prefix="/cloud", tags=["cloud"])
def _callback_url(request: Request) -> str:
# Always loopback — the cloud only redirects codes to 127.0.0.1/localhost.
port = request.url.port or 17493
port = request.url.port or 17494
return f"http://127.0.0.1:{port}/cloud/callback"
@@ -45,14 +45,14 @@ async def cloud_callback(
heading = "You're connected" if ok else "Couldn't connect"
accent = "#16a34a" if ok else "#dc2626"
sub = (
"Voicebox is now linked to your account. You can close this tab and return to the app."
"TalkBox is now linked to your account. You can close this tab and return to the app."
if ok
else message
)
html = f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Voicebox Cloud</title>
<title>TalkBox Cloud</title>
<style>
body {{ margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; background:#0b0b0d; color:#e7e7ea; }}
+3 -3
View File
@@ -29,7 +29,7 @@ async def root():
index = _frontend_dir / "index.html"
if index.is_file():
return FileResponse(index, media_type="text/html")
return {"message": "voicebox API", "version": __version__}
return {"message": "talkbox API", "version": __version__}
@router.post("/shutdown")
@@ -185,7 +185,7 @@ async def health():
gpu_type=gpu_type,
vram_used_mb=vram_used,
backend_type=backend_type,
backend_variant=os.environ.get("VOICEBOX_BACKEND_VARIANT", default_variant),
backend_variant=os.environ.get("TALKBOX_BACKEND_VARIANT", default_variant),
supports_rocm=is_amd_gpu_windows(),
gpu_compatibility_warning=gpu_compat_warning,
)
@@ -211,7 +211,7 @@ async def filesystem_health():
writable = False
error = None
if exists:
probe = dir_path / ".voicebox_probe"
probe = dir_path / ".talkbox_probe"
try:
probe.write_text("ok")
probe.unlink()
+1 -1
View File
@@ -153,7 +153,7 @@ async def export_generation(
safe_text = "generation"
# Append a short id so exports of similarly-worded generations don't collide
# on the same filename (the first 30 chars are frequently identical).
filename = f"generation-{safe_text}-{generation_id[:8]}.voicebox.zip"
filename = f"generation-{safe_text}-{generation_id[:8]}.talkbox.zip"
return StreamingResponse(
io.BytesIO(zip_bytes),
+2 -2
View File
@@ -2,8 +2,8 @@
The Settings UI uses these to let users configure distinct voices per
agent (Claude Code in Morgan, Cursor in Scarlett, ...). The ``client_id``
column is the same value the MCP client sends in ``X-Voicebox-Client-Id``
(or the stdio shim pulls from ``VOICEBOX_CLIENT_ID``).
column is the same value the MCP client sends in ``X-TalkBox-Client-Id``
(or the stdio shim pulls from ``TALKBOX_CLIENT_ID``).
"""
from datetime import datetime, timezone
+1 -1
View File
@@ -294,7 +294,7 @@ async def export_profile(
safe_name = "".join(c for c in profile.name if c.isalnum() or c in (" ", "-", "_")).strip()
if not safe_name:
safe_name = "profile"
filename = f"profile-{safe_name}.voicebox.zip"
filename = f"profile-{safe_name}.talkbox.zip"
return StreamingResponse(
io.BytesIO(zip_bytes),
+5 -5
View File
@@ -1,9 +1,9 @@
"""POST /speak — REST wrapper around voicebox.speak for non-MCP callers.
"""POST /speak — REST wrapper around talkbox.speak for non-MCP callers.
Shell scripts, ACP, A2A, or any agent that doesn't speak MCP can hit this
endpoint to play text through a cloned voice. Uses the same profile
resolution and generation pipeline as the MCP tool, so per-client
bindings (via X-Voicebox-Client-Id) work identically.
bindings (via X-TalkBox-Client-Id) work identically.
"""
from __future__ import annotations
@@ -30,13 +30,13 @@ async def speak(
request: Request,
db: Session = Depends(get_db),
):
"""Speak text in a voice profile. Mirrors voicebox.speak (MCP).
"""Speak text in a voice profile. Mirrors talkbox.speak (MCP).
Response shape matches POST /generate a ``GenerationResponse`` with
``status="generating"`` and an ``id`` the caller polls at
``GET /generate/{id}/status``.
"""
client_id = request.headers.get("X-Voicebox-Client-Id")
client_id = request.headers.get("X-TalkBox-Client-Id")
profile = resolve_profile(data.profile, client_id, db)
if profile is None:
if data.profile:
@@ -48,7 +48,7 @@ async def speak(
status_code=400,
detail=(
"No voice profile resolved. Pass `profile` (name or id), "
"or configure a default in Voicebox → Settings → MCP."
"or configure a default in TalkBox → Settings → MCP."
),
)
+10 -10
View File
@@ -1,5 +1,5 @@
"""
Entry point for PyInstaller-bundled voicebox server.
Entry point for PyInstaller-bundled talkbox server.
This module provides an entry point that works with PyInstaller by using
absolute imports instead of relative imports.
@@ -45,18 +45,18 @@ if getattr(sys, 'frozen', False):
# version check doesn't block for 30+ seconds loading torch etc.
if "--version" in sys.argv:
from backend import __version__
print(f"voicebox-server {__version__}")
print(f"talkbox-server {__version__}")
sys.exit(0)
# Detect backend variant from binary name BEFORE importing backend modules
# so that env-var guards in app.py (e.g. HSA_OVERRIDE_GFX_VERSION) fire at import time.
_binary_name = os.path.basename(sys.executable).lower()
if re.search(r"voicebox-server-rocm(\.exe)?$", _binary_name):
os.environ["VOICEBOX_BACKEND_VARIANT"] = "rocm"
elif re.search(r"voicebox-server-cuda(\.exe)?$", _binary_name):
os.environ["VOICEBOX_BACKEND_VARIANT"] = "cuda"
if re.search(r"talkbox-server-rocm(\.exe)?$", _binary_name):
os.environ["TALKBOX_BACKEND_VARIANT"] = "rocm"
elif re.search(r"talkbox-server-cuda(\.exe)?$", _binary_name):
os.environ["TALKBOX_BACKEND_VARIANT"] = "cuda"
else:
os.environ.setdefault("VOICEBOX_BACKEND_VARIANT", "cpu")
os.environ.setdefault("TALKBOX_BACKEND_VARIANT", "cpu")
import logging
@@ -71,7 +71,7 @@ logger = logging.getLogger(__name__)
# Log startup immediately to confirm binary execution
logger.info("=" * 60)
logger.info("voicebox-server starting up...")
logger.info("talkbox-server starting up...")
logger.info(f"Python version: {sys.version}")
logger.info(f"Executable: {sys.executable}")
logger.info(f"Arguments: {sys.argv}")
@@ -237,7 +237,7 @@ def _start_parent_watchdog(parent_pid, data_dir=None):
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="voicebox backend server")
parser = argparse.ArgumentParser(description="talkbox backend server")
parser.add_argument(
"--host",
type=str,
@@ -272,7 +272,7 @@ if __name__ == "__main__":
if args.parent_pid is not None and args.parent_pid <= 0:
parser.error("--parent-pid must be a positive integer")
logger.info(f"Backend variant: {os.environ.get('VOICEBOX_BACKEND_VARIANT', 'cpu').upper()}")
logger.info(f"Backend variant: {os.environ.get('TALKBOX_BACKEND_VARIANT', 'cpu').upper()}")
# Register parent watchdog to start after server is fully ready
if args.parent_pid is not None:
+7 -7
View File
@@ -1,9 +1,9 @@
"""
Voicebox Cloud device login the "Log in with browser" flow.
TalkBox Cloud device login the "Log in with browser" flow.
The desktop opens the browser to ``{web}/connect``; the user authorizes while
signed in; the cloud redirects a single-use code back to this backend's loopback
callback. We exchange that code (server-to-server, over TLS) for a ``voicebox_``
callback. We exchange that code (server-to-server, over TLS) for a ``talkbox_``
API key, verify the key against the API, and store it locally. The key never
travels through a browser URL, and an unfinished flow leaves nothing behind.
@@ -97,11 +97,11 @@ async def handle_callback(db: Session, code: str, state: str) -> tuple[bool, str
payload = _json_dict(exchanged)
if payload is None:
logger.warning("cloud exchange returned a non-JSON payload")
return False, "Voicebox Cloud returned an unexpected response."
return False, "TalkBox Cloud returned an unexpected response."
api_key = payload.get("key")
device_name = payload.get("label")
if not api_key:
return False, "Voicebox Cloud did not return a key."
return False, "TalkBox Cloud did not return a key."
# Confirm the freshly minted key actually authenticates the API.
me = await client.get(
@@ -116,10 +116,10 @@ async def handle_callback(db: Session, code: str, state: str) -> tuple[bool, str
account_user_id = data.get("userId") if isinstance(data, dict) else None
except httpx.HTTPError:
logger.exception("network error during cloud exchange")
return False, "Could not reach Voicebox Cloud. Check your connection and try again."
return False, "Could not reach TalkBox Cloud. Check your connection and try again."
_store_key(db, api_key=api_key, device_name=device_name, account_user_id=account_user_id)
logger.info("connected to Voicebox Cloud as device %r", device_name)
logger.info("connected to TalkBox Cloud as device %r", device_name)
return True, "Connected"
@@ -154,7 +154,7 @@ def get_status(db: Session) -> dict:
"""Local view of the cloud link — never returns the full key."""
row = _get_or_create_row(db)
connected = bool(row.api_key)
# Prefix only: "voicebox_" (9) + 8 chars, matching the cloud's key_prefix.
# Prefix only: "talkbox_" (9) + 8 chars, matching the cloud's key_prefix.
key_prefix = row.api_key[:17] if row.api_key else None
return {
"connected": connected,
+8 -8
View File
@@ -2,7 +2,7 @@
CUDA backend download, assembly, and verification.
Downloads two archives from GitHub Releases:
1. Server core (voicebox-server-cuda.tar.gz) the exe + non-NVIDIA deps,
1. Server core (talkbox-server-cuda.tar.gz) the exe + non-NVIDIA deps,
versioned with the app.
2. CUDA libs (cuda-libs-{version}.tar.gz) NVIDIA runtime libraries,
versioned independently (only redownloaded on CUDA toolkit bump).
@@ -27,7 +27,7 @@ from ..utils.progress import get_progress_manager
logger = logging.getLogger(__name__)
GITHUB_RELEASES_URL = "https://github.com/jamiepine/voicebox/releases/download"
GITHUB_RELEASES_URL = "https://github.com/jamiepine/talkbox/releases/download"
PROGRESS_KEY = "cuda-backend"
@@ -61,8 +61,8 @@ def get_cuda_dir() -> Path:
def get_cuda_exe_name() -> str:
"""Platform-specific CUDA executable filename."""
if sys.platform == "win32":
return "voicebox-server-cuda.exe"
return "voicebox-server-cuda"
return "talkbox-server-cuda.exe"
return "talkbox-server-cuda"
def is_cuda_download_supported() -> bool:
@@ -115,7 +115,7 @@ def is_cuda_active() -> bool:
The CUDA binary sets this env var on startup (see server.py).
"""
return os.environ.get("VOICEBOX_BACKEND_VARIANT") == "cuda"
return os.environ.get("TALKBOX_BACKEND_VARIANT") == "cuda"
def get_cuda_status() -> dict:
@@ -312,7 +312,7 @@ async def _download_cuda_binary_locked(version: Optional[str] = None):
)
base_url = f"{GITHUB_RELEASES_URL}/{version}"
server_archive = "voicebox-server-cuda.tar.gz"
server_archive = "talkbox-server-cuda.tar.gz"
libs_archive = f"cuda-libs-{CUDA_LIBS_VERSION}.tar.gz"
try:
@@ -394,9 +394,9 @@ def get_cuda_binary_version() -> Optional[str]:
timeout=30,
cwd=str(cuda_path.parent), # Run from the onedir directory
)
# Output format: "voicebox-server 0.3.0"
# Output format: "talkbox-server 0.3.0"
for line in result.stdout.strip().splitlines():
if "voicebox-server" in line:
if "talkbox-server" in line:
return line.split()[-1]
except Exception as e:
logger.warning(f"Could not get CUDA binary version: {e}")
+8 -8
View File
@@ -2,7 +2,7 @@
ROCm backend download, assembly, and verification.
Downloads two archives from GitHub Releases:
1. Server core (voicebox-server-rocm.tar.gz) the exe + non-AMD deps,
1. Server core (talkbox-server-rocm.tar.gz) the exe + non-AMD deps,
versioned with the app.
2. ROCm libs (rocm-libs-{version}.tar.gz) AMD runtime libraries,
versioned independently (only redownloaded on ROCm toolkit bump).
@@ -28,7 +28,7 @@ from .. import __version__
logger = logging.getLogger(__name__)
GITHUB_RELEASES_URL = "https://github.com/jamiepine/voicebox/releases/download"
GITHUB_RELEASES_URL = "https://github.com/jamiepine/talkbox/releases/download"
PROGRESS_KEY = "rocm-backend"
@@ -60,8 +60,8 @@ def get_rocm_dir() -> Path:
def get_rocm_exe_name() -> str:
"""Platform-specific ROCm executable filename."""
if sys.platform == "win32":
return "voicebox-server-rocm.exe"
return "voicebox-server-rocm"
return "talkbox-server-rocm.exe"
return "talkbox-server-rocm"
def get_rocm_binary_path() -> Optional[Path]:
@@ -95,7 +95,7 @@ def is_rocm_active() -> bool:
The ROCm binary sets this env var on startup (see server.py).
"""
return os.environ.get("VOICEBOX_BACKEND_VARIANT") == "rocm"
return os.environ.get("TALKBOX_BACKEND_VARIANT") == "rocm"
def get_rocm_status() -> dict:
@@ -287,7 +287,7 @@ async def _download_rocm_binary_locked(version: Optional[str] = None):
# release tag; the libs content version is encoded in the filename only.
server_base_url = f"{GITHUB_RELEASES_URL}/{version}"
libs_base_url = server_base_url
server_archive = "voicebox-server-rocm.tar.gz"
server_archive = "talkbox-server-rocm.tar.gz"
libs_archive = f"rocm-libs-{ROCM_LIBS_VERSION}.tar.gz"
# Always stage when any download is needed, then atomically rename over
@@ -409,9 +409,9 @@ def get_rocm_binary_version() -> Optional[str]:
timeout=30,
cwd=str(rocm_path.parent), # Run from the onedir directory
)
# Output format: "voicebox-server 0.3.0"
# Output format: "talkbox-server 0.3.0"
for line in result.stdout.strip().splitlines():
if "voicebox-server" in line:
if "talkbox-server" in line:
return line.split()[-1]
except Exception as e:
logger.warning(f"Could not get ROCm binary version: {e}")
+8 -8
View File
@@ -48,10 +48,10 @@ Search order — **first hit wins**:
| Platform | Path | Build type |
|----------|------|------------|
| macOS | `backend/dist/voicebox-server-cuda/voicebox-server-cuda` | onedir (CUDA, rarely on Mac) |
| macOS | `backend/dist/voicebox-server` | onefile (CPU) |
| Windows | `backend\dist\voicebox-server-cuda\voicebox-server-cuda.exe` | onedir (CUDA) |
| Windows | `backend\dist\voicebox-server.exe` | onefile (CPU) |
| macOS | `backend/dist/talkbox-server-cuda/talkbox-server-cuda` | onedir (CUDA, rarely on Mac) |
| macOS | `backend/dist/talkbox-server` | onefile (CPU) |
| Windows | `backend\dist\talkbox-server-cuda\talkbox-server-cuda.exe` | onedir (CUDA) |
| Windows | `backend\dist\talkbox-server.exe` | onefile (CPU) |
If none exist, run `python backend/build_binary.py` and wait for it to finish (can take 5-20 min). Fail with a clear error if the build itself fails. `--skip-build` flag forces "error out if no binary" instead of building.
@@ -64,7 +64,7 @@ Mirrors Tauri's launch in `tauri/src-tauri/src/main.rs:369-388`:
```
- **Port**: bind to `0` first in Python to grab a free port, then pass that number.
- **Data dir**: `tempfile.mkdtemp(prefix="voicebox-e2e-")`. Deleted after the run unless `--keep-data-dir`. Profiles and generated WAVs land here.
- **Data dir**: `tempfile.mkdtemp(prefix="talkbox-e2e-")`. Deleted after the run unless `--keep-data-dir`. Profiles and generated WAVs land here.
- **Parent PID**: current Python PID — ensures the backend dies if the test crashes (watchdog in `server.py:102-224`).
- **stdout/stderr**: tee to both a log file in `./results/server-<timestamp>.log` and a rolling in-memory buffer. On model failure, last 100 lines of the buffer are attached to that model's error record.
@@ -136,7 +136,7 @@ On timeout: cancel the SSE stream, mark the row `timeout`, and continue to the n
```json
{
"platform": "darwin-arm64",
"binary": "/abs/path/voicebox-server",
"binary": "/abs/path/talkbox-server",
"binary_size_mb": 612,
"started_at": "2026-04-16T12:34:56Z",
"finished_at": "...",
@@ -160,7 +160,7 @@ On timeout: cancel the SSE stream, mark the row `timeout`, and continue to the n
Companion `./results/e2e-<...>.md`:
```
# Voicebox E2E — darwin-arm64 — 2026-04-16 12:34
# TalkBox E2E — darwin-arm64 — 2026-04-16 12:34
| Engine | Size | Status | Elapsed | Error |
|---------------------|------|--------|---------|-------|
@@ -208,7 +208,7 @@ The script uses only stdlib + `httpx` (or `requests`) + `sseclient-py` — all a
- Always kill the spawned binary in a `try/finally`. On Windows, `taskkill /F /T` the whole tree (Tauri does the same).
- Verify the port is free on shutdown (Tauri port-reuse check in `main.rs:114-186` could otherwise pick up a ghost).
- Don't touch the user's HF cache by default — let the server use `HF_HUB_CACHE` / `VOICEBOX_MODELS_DIR`. Passing `--isolated-cache` would point both env vars at the tempdir for a true cold-start run (opt-in only; would re-download every time).
- Don't touch the user's HF cache by default — let the server use `HF_HUB_CACHE` / `TALKBOX_MODELS_DIR`. Passing `--isolated-cache` would point both env vars at the tempdir for a true cold-start run (opt-in only; would re-download every time).
## Non-goals
+1 -1
View File
@@ -1,5 +1,5 @@
"""
Test suite for Voicebox backend.
Test suite for TalkBox backend.
This directory contains manual test scripts for debugging and validating
progress tracking, model downloads, and generation functionality.
+6 -6
View File
@@ -96,8 +96,8 @@ def find_binary() -> Optional[Path]:
is_win = platform.system() == "Windows"
exe = ".exe" if is_win else ""
candidates = [
DIST_DIR / "voicebox-server-cuda" / f"voicebox-server-cuda{exe}",
DIST_DIR / f"voicebox-server{exe}",
DIST_DIR / "talkbox-server-cuda" / f"talkbox-server-cuda{exe}",
DIST_DIR / f"talkbox-server{exe}",
]
for c in candidates:
if c.exists() and c.is_file():
@@ -381,7 +381,7 @@ def write_reports(
json_path.write_text(json.dumps(doc, indent=2))
lines = [
f"# Voicebox E2E — {plat}{started_at.strftime('%Y-%m-%d %H:%M UTC')}",
f"# TalkBox E2E — {plat}{started_at.strftime('%Y-%m-%d %H:%M UTC')}",
"",
f"Binary: `{binary}` ",
f"Elapsed: {doc['elapsed_seconds']:.1f}s",
@@ -424,8 +424,8 @@ def write_reports(
# ── Main ─────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Voicebox E2E model generation test")
p.add_argument("--binary", type=Path, help="Path to voicebox-server binary (overrides auto-detect)")
p = argparse.ArgumentParser(description="TalkBox E2E model generation test")
p.add_argument("--binary", type=Path, help="Path to talkbox-server binary (overrides auto-detect)")
p.add_argument("--skip-build", action="store_true", help="Error if binary missing instead of building")
p.add_argument(
"--reference-wav",
@@ -516,7 +516,7 @@ def main() -> int:
print(f"[fixture] reference text: {ref_text!r}", flush=True)
# Tempdir + log path
data_dir = Path(tempfile.mkdtemp(prefix="voicebox-e2e-"))
data_dir = Path(tempfile.mkdtemp(prefix="talkbox-e2e-"))
args.output_dir.mkdir(parents=True, exist_ok=True)
ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
log_path = args.output_dir / f"server-{ts}.log"
+7 -7
View File
@@ -2,7 +2,7 @@
Tests for CORS origin restrictions.
Validates that the CORS middleware only allows known local origins
and respects the VOICEBOX_CORS_ORIGINS environment variable.
and respects the TALKBOX_CORS_ORIGINS environment variable.
Uses a minimal FastAPI app that mirrors the exact CORS configuration
from backend/main.py, so tests run without heavy ML dependencies.
@@ -32,8 +32,8 @@ def _build_app(env_origins: str = "") -> FastAPI:
_default_origins = [
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:17493",
"http://127.0.0.1:17493",
"http://localhost:17494",
"http://127.0.0.1:17494",
"tauri://localhost",
"https://tauri.localhost",
]
@@ -88,8 +88,8 @@ class TestCORSDefaultOrigins:
@pytest.mark.parametrize("origin", [
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:17493",
"http://127.0.0.1:17493",
"http://localhost:17494",
"http://127.0.0.1:17494",
"tauri://localhost",
"https://tauri.localhost",
])
@@ -121,7 +121,7 @@ class TestCORSDefaultOrigins:
class TestCORSCustomOrigins:
"""VOICEBOX_CORS_ORIGINS env var should extend the allowlist."""
"""TALKBOX_CORS_ORIGINS env var should extend the allowlist."""
def test_custom_origin_allowed(self, client_with_custom_origins):
headers = _get_with_origin(client_with_custom_origins, "https://custom.example.com")
@@ -141,7 +141,7 @@ class TestCORSCustomOrigins:
class TestCORSEnvVarParsing:
"""Edge cases for VOICEBOX_CORS_ORIGINS parsing."""
"""Edge cases for TALKBOX_CORS_ORIGINS parsing."""
def test_empty_env_var(self):
app = _build_app("")
+1 -1
View File
@@ -1,4 +1,4 @@
"""Tests for the voicebox.speak MCP tool's ``model_size`` plumbing (issue #884).
"""Tests for the talkbox.speak MCP tool's ``model_size`` plumbing (issue #884).
The MCP speak path used to build its ``GenerationRequest`` without a
``model_size``, so every agent-triggered generation silently fell back to the
+8 -8
View File
@@ -45,7 +45,7 @@ class TestIsRocmFile:
@pytest.mark.parametrize(
"rel_path",
[
"voicebox-server-rocm.exe",
"talkbox-server-rocm.exe",
"_internal/python312.dll",
"_internal/torch/lib/torch_cpu.dll",
"_internal/torch/lib/c10.dll",
@@ -68,8 +68,8 @@ class TestPackage:
"""End-to-end split of a synthetic onedir into the two archives."""
def test_split_and_manifest(self, tmp_path):
onedir = tmp_path / "voicebox-server-rocm"
_write(onedir / "voicebox-server-rocm.exe")
onedir = tmp_path / "talkbox-server-rocm"
_write(onedir / "talkbox-server-rocm.exe")
_write(onedir / "_internal" / "python312.dll")
_write(onedir / "_internal" / "rocm_sdk" / "__init__.py")
_write(onedir / "_internal" / "torch" / "lib" / "torch_cpu.dll")
@@ -88,11 +88,11 @@ class TestPackage:
out = tmp_path / "release-assets"
package_rocm.package(onedir, out, "rocm7.2-v1", ">=2.9.0,<2.10.0")
server = out / "voicebox-server-rocm.tar.gz"
server = out / "talkbox-server-rocm.tar.gz"
libs = out / "rocm-libs-rocm7.2-v1.tar.gz"
assert server.exists()
assert libs.exists()
assert (out / "voicebox-server-rocm.tar.gz.sha256").exists()
assert (out / "talkbox-server-rocm.tar.gz.sha256").exists()
assert (out / "rocm-libs-rocm7.2-v1.tar.gz.sha256").exists()
with tarfile.open(libs) as tar:
@@ -106,15 +106,15 @@ class TestPackage:
"_internal/_rocm_sdk_libraries_custom/lib/rocblas/library/TensileLibrary.dat"
in lib_names
)
assert "voicebox-server-rocm.exe" in core_names
assert "talkbox-server-rocm.exe" in core_names
assert "_internal/torch/lib/torch_cpu.dll" in core_names
assert "_internal/rocm_sdk/__init__.py" in core_names
# Archives must be disjoint.
assert lib_names.isdisjoint(core_names)
def test_empty_rocm_set_exits(self, tmp_path):
onedir = tmp_path / "voicebox-server-rocm"
_write(onedir / "voicebox-server-rocm.exe")
onedir = tmp_path / "talkbox-server-rocm"
_write(onedir / "talkbox-server-rocm.exe")
_write(onedir / "_internal" / "torch" / "lib" / "torch_cpu.dll")
with pytest.raises(SystemExit):
+2 -2
View File
@@ -177,7 +177,7 @@ def score(
# ── Runner ────────────────────────────────────────────────────────────
DEFAULT_PORTS = (8000, 8765, 8899, 17493)
DEFAULT_PORTS = (8000, 8765, 8899, 17494)
THROWAWAY_PROFILE_PREFIX = "personality-harness-"
KOKORO_PROBE_VOICE = "af_heart"
"""Any valid kokoro voice id works — compose never calls into TTS, it
@@ -204,7 +204,7 @@ def detect_backend_port(hint: Optional[int]) -> int:
except Exception:
continue
raise SystemExit(
"No running Voicebox backend found. Start it (`python backend/main.py`) "
"No running TalkBox backend found. Start it (`python backend/main.py`) "
f"or pass --port. Tried: {candidates}"
)
+1 -1
View File
@@ -252,7 +252,7 @@ async def test_full_integration():
async def main():
"""Run all tests."""
print("\n" + "=" * 60)
print("Voicebox Progress Tracking Test Suite")
print("TalkBox Progress Tracking Test Suite")
print("=" * 60)
results = []
+4 -4
View File
@@ -15,7 +15,7 @@ Usage:
python backend/tests/test_refinement_samples.py
# Hit a non-default port (auto-detected via /health probe when omitted):
python backend/tests/test_refinement_samples.py --port 17493
python backend/tests/test_refinement_samples.py --port 17494
# Only test one model size:
python backend/tests/test_refinement_samples.py --model 4B
@@ -316,7 +316,7 @@ def score(sample: Sample, model: str, refined: str, latency_ms: int) -> Scorecar
# ── Runner ────────────────────────────────────────────────────────────
DEFAULT_PORTS = (8000, 8765, 8899, 17493)
DEFAULT_PORTS = (8000, 8765, 8899, 17494)
def detect_backend_port(hint: Optional[int]) -> int:
@@ -339,7 +339,7 @@ def detect_backend_port(hint: Optional[int]) -> int:
except Exception:
continue
raise SystemExit(
"No running Voicebox backend found. Start it (`python backend/main.py`) "
"No running TalkBox backend found. Start it (`python backend/main.py`) "
f"or pass --port. Tried: {candidates}"
)
@@ -407,7 +407,7 @@ def format_report(cards: list[Scorecard]) -> str:
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--port", type=int, default=None,
help="Voicebox backend port (auto-detected if omitted)")
help="TalkBox backend port (auto-detected if omitted)")
ap.add_argument("--model", choices=("0.6B", "1.7B", "4B"), action="append",
help="Refinement model size(s) to test (repeat to run several)")
ap.add_argument("--json", type=Path, default=None,
+3 -3
View File
@@ -35,7 +35,7 @@ class TestRocmBuildArgs:
def test_binary_name(self, captured_args):
idx = captured_args.index("--name")
assert captured_args[idx + 1] == "voicebox-server-rocm"
assert captured_args[idx + 1] == "talkbox-server-rocm"
def test_pack_mode_is_onedir(self, captured_args):
assert "--onedir" in captured_args
@@ -91,8 +91,8 @@ class TestRocmBuildE2E:
backend_dir = Path(__file__).parent.parent
build_script = backend_dir / "build_binary.py"
dist_dir = backend_dir / "dist"
binary_dir = dist_dir / "voicebox-server-rocm"
binary_exe = binary_dir / "voicebox-server-rocm.exe"
binary_dir = dist_dir / "talkbox-server-rocm"
binary_exe = binary_dir / "talkbox-server-rocm.exe"
# Clean previous dist if it exists to ensure a fresh build
if binary_dir.exists():
+10 -10
View File
@@ -40,7 +40,7 @@ def fake_tar_gz():
buf = BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
data = b"fake binary content"
info = tarfile.TarInfo(name="voicebox-server-rocm.exe")
info = tarfile.TarInfo(name="talkbox-server-rocm.exe")
info.size = len(data)
tar.addfile(info, BytesIO(data))
buf.seek(0)
@@ -137,18 +137,18 @@ async def test_download_rocm_binary_progress_reporting(mock_backends_dir, fake_t
libs_sha = hashlib.sha256(fake_tar_gz).hexdigest()
responses = {
"https://github.com/jamiepine/voicebox/releases/download/v0.2.3/voicebox-server-rocm.tar.gz": FakeResponse(
"https://github.com/jamiepine/talkbox/releases/download/v0.2.3/talkbox-server-rocm.tar.gz": FakeResponse(
content=fake_tar_gz,
headers={"content-length": str(len(fake_tar_gz))},
),
"https://github.com/jamiepine/voicebox/releases/download/v0.2.3/voicebox-server-rocm.tar.gz.sha256": FakeResponse(
content=f"{server_sha} voicebox-server-rocm.tar.gz\n".encode(),
"https://github.com/jamiepine/talkbox/releases/download/v0.2.3/talkbox-server-rocm.tar.gz.sha256": FakeResponse(
content=f"{server_sha} talkbox-server-rocm.tar.gz\n".encode(),
),
f"https://github.com/jamiepine/voicebox/releases/download/v0.2.3/rocm-libs-{rocm.ROCM_LIBS_VERSION}.tar.gz": FakeResponse(
f"https://github.com/jamiepine/talkbox/releases/download/v0.2.3/rocm-libs-{rocm.ROCM_LIBS_VERSION}.tar.gz": FakeResponse(
content=fake_tar_gz,
headers={"content-length": str(len(fake_tar_gz))},
),
f"https://github.com/jamiepine/voicebox/releases/download/v0.2.3/rocm-libs-{rocm.ROCM_LIBS_VERSION}.tar.gz.sha256": FakeResponse(
f"https://github.com/jamiepine/talkbox/releases/download/v0.2.3/rocm-libs-{rocm.ROCM_LIBS_VERSION}.tar.gz.sha256": FakeResponse(
content=f"{libs_sha} rocm-libs.tar.gz\n".encode(),
),
}
@@ -160,7 +160,7 @@ async def test_download_rocm_binary_progress_reporting(mock_backends_dir, fake_t
# Verify extraction
rocm_dir = rocm.get_rocm_dir()
assert (rocm_dir / "voicebox-server-rocm.exe").exists()
assert (rocm_dir / "talkbox-server-rocm.exe").exists()
# Verify manifest written
manifest_path = rocm.get_rocm_libs_manifest_path()
@@ -177,13 +177,13 @@ async def test_download_rocm_binary_progress_reporting(mock_backends_dir, fake_t
@pytest.mark.asyncio
async def test_is_rocm_active(mock_backends_dir, monkeypatch):
monkeypatch.setenv("VOICEBOX_BACKEND_VARIANT", "rocm")
monkeypatch.setenv("TALKBOX_BACKEND_VARIANT", "rocm")
assert rocm.is_rocm_active() is True
monkeypatch.setenv("VOICEBOX_BACKEND_VARIANT", "cpu")
monkeypatch.setenv("TALKBOX_BACKEND_VARIANT", "cpu")
assert rocm.is_rocm_active() is False
monkeypatch.delenv("VOICEBOX_BACKEND_VARIANT", raising=False)
monkeypatch.delenv("TALKBOX_BACKEND_VARIANT", raising=False)
assert rocm.is_rocm_active() is False
+2 -2
View File
@@ -65,8 +65,8 @@ class TestRocmRequirements:
@pytest.mark.timeout(900)
@pytest.mark.skipif(
not os.environ.get("VOICEBOX_TEST_ROCM_INSTALL"),
reason="Set VOICEBOX_TEST_ROCM_INSTALL=1 to run the heavy install test",
not os.environ.get("TALKBOX_TEST_ROCM_INSTALL"),
reason="Set TALKBOX_TEST_ROCM_INSTALL=1 to run the heavy install test",
)
def test_rocm_torch_installs_and_detects_amd(self, backend_dir):
"""
+1 -1
View File
@@ -15,7 +15,7 @@ from typing import List, Tuple
import numpy as np
logger = logging.getLogger("voicebox.chunked-tts")
logger = logging.getLogger("talkbox.chunked-tts")
# Default chunk size in characters. Can be overridden per-request via
# the ``max_chunk_chars`` field on GenerationRequest.
+2 -2
View File
@@ -154,7 +154,7 @@ def patch_transformers_mistral_regex():
variant. That call raises on ``HF_HUB_OFFLINE=1`` and on plain network
failures, killing unrelated loads (Qwen TTS, TADA, etc.).
Voicebox never loads Mistral models, so the rewrite the function would
TalkBox never loads Mistral models, so the rewrite the function would
apply is a no-op for us anyway. Wrap the method so any exception from the
metadata lookup returns the tokenizer unchanged matching the success-path
behavior for non-Mistral repos (transformers 4.57.3,
@@ -264,7 +264,7 @@ def ensure_original_qwen_config_cached():
logger.warning("could not create cache symlink for %s", original_repo, exc_info=True)
if os.environ.get("VOICEBOX_OFFLINE_PATCH", "1") != "0":
if os.environ.get("TALKBOX_OFFLINE_PATCH", "1") != "0":
patch_huggingface_hub_offline()
patch_transformers_mistral_regex()
ensure_original_qwen_config_cached()