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:
+8
-8
@@ -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
@@ -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
@@ -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
@@ -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("/")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
"""Route registration for the voicebox API."""
|
||||
"""Route registration for the talkbox API."""
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
||||
@@ -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; }}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user