Files
voicebox/backend/routes/speak.py
T
Jamie Pine abf5dfda8c personality: bool API, i18n across the app
- Collapse intent tri-state (respond/rewrite/compose) to `personality: bool` on /generate, /speak, and voicebox.speak. Drop respond entirely; keep compose as a standalone button via /profiles/{id}/compose. Remove /rewrite, /respond, and /speak profile endpoints.
- FloatingGenerateBox: Wand2 persona toggle + Dices compose button appear when the selected profile has a personality. ProfileCard badges Wand2 alongside the effects Sparkles.
- MCP bindings: default_intent column → default_personality: bool. Migration drops the legacy column.
- i18n: en / ja / zh-CN / zh-TW translation files filled out and wired through the capture, server, and profile UI.

```ts
voicebox.speak({
  text: "Deploy complete.",
  profile: "Morgan",
  personality: true, // rewrite through the profile's personality LLM
});
```
2026-04-23 17:31:23 -07:00

89 lines
2.7 KiB
Python

"""POST /speak — REST wrapper around voicebox.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.
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from .. import models
from ..database import MCPClientBinding, get_db
from ..mcp_server import events as mcp_events
from ..mcp_server.resolve import resolve_profile
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/speak", response_model=models.GenerationResponse)
async def speak(
data: models.SpeakRequest,
request: Request,
db: Session = Depends(get_db),
):
"""Speak text in a voice profile. Mirrors voicebox.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")
profile = resolve_profile(data.profile, client_id, db)
if profile is None:
if data.profile:
raise HTTPException(
status_code=404,
detail=f"Voice profile '{data.profile}' not found.",
)
raise HTTPException(
status_code=400,
detail=(
"No voice profile resolved. Pass `profile` (name or id), "
"or configure a default in Voicebox → Settings → MCP."
),
)
# Resolve per-client personality default when the caller didn't pin it.
personality_flag = data.personality
if personality_flag is None and client_id:
binding = (
db.query(MCPClientBinding)
.filter(MCPClientBinding.client_id == client_id)
.first()
)
if binding is not None:
personality_flag = bool(binding.default_personality)
from .generations import generate_speech
generation = await generate_speech(
models.GenerationRequest(
profile_id=profile.id,
text=data.text,
language=data.language or "en",
engine=data.engine or "qwen",
personality=bool(personality_flag),
),
db,
)
mcp_events.publish(
"speak-start",
{
"generation_id": getattr(generation, "id", None),
"profile_name": profile.name,
"source": "rest",
"client_id": client_id,
},
)
return generation