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
});
```
This commit is contained in:
Jamie Pine
2026-04-23 17:31:23 -07:00
parent 7c50e189cb
commit abf5dfda8c
41 changed files with 2146 additions and 1193 deletions
+18 -49
View File
@@ -81,6 +81,10 @@ class GenerationRequest(BaseModel):
model_size: Optional[str] = Field(default="1.7B", pattern="^(1\\.7B|0\\.6B|1B|3B)$")
instruct: Optional[str] = Field(None, max_length=500)
engine: Optional[str] = Field(default="qwen", pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$")
personality: bool = Field(
default=False,
description="When true and the profile has a personality prompt, the input text is rewritten in-character before TTS.",
)
max_chunk_chars: int = Field(
default=800, ge=100, le=5000, description="Max characters per chunk for long text splitting"
)
@@ -297,8 +301,9 @@ class GenerationSettingsUpdate(BaseModel):
class MCPClientBindingResponse(BaseModel):
"""Per-MCP-client voice binding — what voice / engine / intent the server
should use when a given client_id calls voicebox.speak without args."""
"""Per-MCP-client voice binding — what voice / engine the server should
use when a given client_id calls voicebox.speak without args, plus an
opt-in personality-rewrite default."""
client_id: str
label: Optional[str] = None
@@ -307,9 +312,7 @@ class MCPClientBindingResponse(BaseModel):
None,
pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$",
)
default_intent: Optional[str] = Field(
None, pattern="^(respond|rewrite|compose)$"
)
default_personality: bool = False
last_seen_at: Optional[datetime] = None
created_at: datetime
updated_at: datetime
@@ -328,9 +331,7 @@ class MCPClientBindingUpsert(BaseModel):
None,
pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$",
)
default_intent: Optional[str] = Field(
None, pattern="^(respond|rewrite|compose)$"
)
default_personality: bool = False
class MCPClientBindingListResponse(BaseModel):
@@ -349,8 +350,9 @@ class SpeakRequest(BaseModel):
None,
pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$",
)
intent: Optional[str] = Field(
None, pattern="^(respond|rewrite|compose)$"
personality: Optional[bool] = Field(
None,
description="When true and the profile has a personality prompt, the input text is rewritten in-character before TTS. When null, the per-client binding's default_personality flag decides.",
)
language: Optional[str] = Field(
None,
@@ -380,53 +382,20 @@ class LLMGenerateResponse(BaseModel):
model_size: str
# ── Profile personality endpoints ─────────────────────────────────────
# compose / rewrite / respond return raw text; /speak chains LLM → TTS
# and either persists as a generation (persist=true) or streams audio
# back transiently.
class PersonalityTextRequest(BaseModel):
"""Body for ``/profiles/{id}/rewrite`` and ``/profiles/{id}/respond``."""
text: str = Field(..., min_length=1, max_length=10000)
# ── Profile personality endpoint ─────────────────────────────────────
# The sole standalone personality endpoint is ``/profiles/{id}/compose``,
# which produces a fresh in-character utterance the UI drops into the
# generate textarea. Rewrite is now reached via ``/generate`` with
# ``personality=true``.
class PersonalityTextResponse(BaseModel):
"""Response returned by compose / rewrite / respond endpoints."""
"""Response returned by the ``/profiles/{id}/compose`` endpoint."""
text: str
model_size: str
class PersonalitySpeakRequest(BaseModel):
"""Body for ``/profiles/{id}/speak`` — LLM transform then TTS."""
text: str = Field(..., min_length=1, max_length=10000)
# When true, the generated audio is persisted as a regular row in the
# generations table (tagged with ``source="personality_speak"``) and
# the response returns a GenerationResponse the client polls like any
# other generation. When false, the LLM output is fed to a synchronous
# TTS call and the wav bytes stream back directly.
persist: bool = True
language: Optional[str] = Field(
None,
pattern="^(zh|en|ja|ko|de|fr|ru|pt|es|it|he|ar|da|el|fi|hi|ms|nl|no|pl|sv|sw|tr)$",
)
engine: Optional[str] = Field(
None,
pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$",
)
# ``respond`` is the default because this endpoint is designed for
# conversational / agent-style callers. Override to ``rewrite`` to
# speak the user's text in character verbatim, or ``compose`` to
# speak an utterance the character would come up with on its own
# (in which case ``text`` is treated as a topical hint, not content).
intent: str = Field(
default="respond", pattern="^(respond|rewrite|compose)$"
)
class ModelReadiness(BaseModel):
"""Per-model entry in the dictation readiness checklist.