diff --git a/CHANGELOG.md b/CHANGELOG.md
index 500ad555..59334545 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,11 +9,9 @@
## [0.5.0] - 2026-04-22
-**The Capture release.** Voicebox stops being a voice-cloning studio and becomes an AI voice studio. The loop closes in both directions: your voice goes into your computer through a global hotkey, and any agent's voice comes out of your computer through a voice you own.
+**The Capture release.** Voicebox stops being just a voice-cloning studio and becomes a full AI voice studio. The loop closes in both directions: your voice goes into your computer through a global hotkey, and any agent's voice comes out of your computer through a voice you own.
-Hold a key anywhere on your machine, speak, release — the transcript lands in the focused text field in whatever app you were using. Flip the primitive around and any MCP-aware agent — Claude Code, Cursor, Cline, Spacebot — speaks back through the same on-screen pill in one of your cloned voices. A local LLM sits between the two, so transcripts come out clean and voice profiles can carry a personality that reshapes what the agent actually says before it gets spoken.
-
-Everything still runs on your hardware. No cloud, no accounts, no audio leaving the machine.
+When enabled, hold a key anywhere on your machine, speak, release — the transcript lands in the focused text field in whatever app you were using. Flip the primitive around and any MCP-aware agent — Claude Code, Cursor, Cline, Spacebot — speaks back through the same on-screen pill in one of your cloned voices. A local LLM sits between the two, so transcripts come out clean and voice profiles can carry a personality that reshapes what the agent actually says before it gets spoken.
### Dictation — speak anywhere, paste anywhere
@@ -36,9 +34,25 @@ Temperatures are tuned per mode (compose hot for variety, rewrite cold for fidel
### Agents — any MCP-aware agent gets a voice
-> **Note:** the MCP server implementation is not yet in this tag. The pieces it depends on — the personality/compose/rewrite/respond runtime, voice-binding-per-profile, and the on-screen pill as a generic surface — are all in place. The MCP route lands in a follow-up commit before the final 0.5.0 tag.
+Voicebox ships a built-in **Model Context Protocol** server at `http://127.0.0.1:17493/mcp` so Claude Code, Cursor, Windsurf, Cline, VS Code MCP extensions — any MCP-aware agent — can call into your local Voicebox install. Four tools ship with dotted names:
-Once the MCP server ships, any MCP-aware agent — Claude Code, Cursor, Cline, Spacebot — can call `voicebox.speak({ profile, text, intent })` and Voicebox will produce in-character speech in the bound voice. The on-screen pill surfaces whenever an agent is talking so you always see what's coming out of your machine. Pin Claude Code to Morgan, Cursor to Scarlett, Spacebot to its own voice — you can tell which agent is speaking without looking.
+- **`voicebox.speak`** — speak text in any voice profile, with optional `intent: compose | rewrite | respond` to run through the profile's personality LLM first
+- **`voicebox.transcribe`** — Whisper transcription of a base64 blob or an absolute local path
+- **`voicebox.list_captures`** — recent captures with their transcripts
+- **`voicebox.list_profiles`** — available voice profiles (cloned + preset)
+
+- **Streamable HTTP as primary transport.** FastMCP mounted inside the existing FastAPI process (Nov-2025 MCP spec, post-SSE). Cursor / Windsurf / VS Code / Claude Code all support it out of the box — drop a `mcpServers` block with the URL and an `X-Voicebox-Client-Id` header.
+- **Stdio shim for clients that don't speak HTTP MCP.** A `voicebox-mcp` binary ships inside the app bundle as a Tauri sidecar — ~18 MB PyInstaller build with torch / transformers / mlx explicitly excluded. The Settings page renders the install snippet with the right absolute path pre-filled.
+- **Per-client voice binding.** Pin Claude Code to Morgan, Cursor to Scarlett, Cline to its own voice — the `X-Voicebox-Client-Id` header resolves to a bound voice whenever `speak` is called without an explicit `profile`. Managed in **Settings → MCP**, with an auto-stamped `last_seen_at` timestamp on each row so you can tell the install actually took. New clients register their binding row on first call.
+- **Profile resolution precedence.** Explicit `profile` arg (name or id, case-insensitive) → per-client binding → global default from `capture_settings.default_playback_voice_id` → error with a pointer to Settings.
+- **Speaking pill.** Agent-initiated speech surfaces the same on-screen pill as dictation, in a new `speaking` state with the profile name and an elapsed timer. Driven by SSE at `/events/speak`; a Tauri-side `dictate:show` handler repositions and reveals the pill over the current monitor even when the main Voicebox window is hidden. Silent background TTS is a trust hazard — the pill always shows what's coming out of your machine.
+- **`POST /speak` REST wrapper.** Same code path and voice resolution for shell scripts, ACP, A2A, GitHub Actions, or anything else that isn't MCP-native.
+
+**Claude Code one-liner:**
+
+```
+claude mcp add voicebox --transport http --url http://127.0.0.1:17493/mcp --header "X-Voicebox-Client-Id: claude-code"
+```
### Refinement hardening
@@ -68,7 +82,7 @@ Settings → Captures is now the home for the whole dictation flow:
### Landing page — /capture
-New `/capture` route tells the Capture story end-to-end. Hero line: *"Just talk to your computer."* Three pillars — multi-engine STT (Whisper, Whisper Turbo, Parakeet v3, Qwen3-ASR), refined transcripts, agents speaking in voices you own. Drop-in MCP config block for Claude Code / Cursor / Cline. Agent-integration section showing per-client voice binding.
+New `/capture` route tells the Capture story end-to-end. Hero line: *"Just talk to your computer."* Three pillars — STT (Whisper, Whisper Turbo), refined transcripts, agents speaking in voices you own. Drop-in MCP config block for Claude Code / Cursor / Cline. Agent-integration section showing per-client voice binding.
## [0.4.5] - 2026-04-22
diff --git a/app/src/components/CapturesTab/CapturesTab.tsx b/app/src/components/CapturesTab/CapturesTab.tsx
index 1d037661..35a853fc 100644
--- a/app/src/components/CapturesTab/CapturesTab.tsx
+++ b/app/src/components/CapturesTab/CapturesTab.tsx
@@ -394,11 +394,11 @@ export function CapturesTab() {
-
+
Captures
Beta
@@ -488,7 +488,7 @@ export function CapturesTab() {
-
+
Whisper {sttModel.charAt(0).toUpperCase() + sttModel.slice(1)}
·
diff --git a/app/src/components/CapturesTab/DictationReadinessChecklist.tsx b/app/src/components/CapturesTab/DictationReadinessChecklist.tsx
index 01623a54..58f7272a 100644
--- a/app/src/components/CapturesTab/DictationReadinessChecklist.tsx
+++ b/app/src/components/CapturesTab/DictationReadinessChecklist.tsx
@@ -54,7 +54,8 @@ function ChecklistRow({ icon, title, description, ready, action }: RowProps) {
function progressPercent(task: ActiveDownloadTask | undefined): number | null {
if (!task) return null;
- if (typeof task.progress === 'number') return Math.max(0, Math.min(100, task.progress));
+ if (typeof task.progress === 'number')
+ return Math.round(Math.max(0, Math.min(100, task.progress)));
if (task.current && task.total) return Math.round((task.current / task.total) * 100);
return null;
}
diff --git a/backend/services/profiles.py b/backend/services/profiles.py
index e6598277..d7d32fa0 100644
--- a/backend/services/profiles.py
+++ b/backend/services/profiles.py
@@ -54,6 +54,7 @@ def _profile_to_response(
preset_voice_id=getattr(profile, "preset_voice_id", None),
design_prompt=getattr(profile, "design_prompt", None),
default_engine=getattr(profile, "default_engine", None),
+ personality=getattr(profile, "personality", None),
generation_count=generation_count,
sample_count=sample_count,
created_at=profile.created_at,
@@ -181,6 +182,7 @@ async def create_profile(
preset_voice_id=data.preset_voice_id,
design_prompt=data.design_prompt,
default_engine=default_engine,
+ personality=data.personality,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
@@ -398,6 +400,7 @@ async def update_profile(
profile.name = data.name
profile.description = data.description
profile.language = data.language
+ profile.personality = data.personality
if data.default_engine is not None:
profile.default_engine = data.default_engine or None # empty string → NULL
profile.updated_at = datetime.utcnow()
diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx
index 59dd1031..b3349477 100644
--- a/landing/src/app/page.tsx
+++ b/landing/src/app/page.tsx
@@ -9,6 +9,7 @@ import {ControlUI} from "@/components/ControlUI";
import {Features} from "@/components/Features";
import {Footer} from "@/components/Footer";
import {Navbar} from "@/components/Navbar";
+import {Personalities} from "@/components/Personalities";
import {AppleIcon, LinuxIcon, WindowsIcon} from "@/components/PlatformIcons";
import {SupportedModels} from "@/components/SupportedModels";
import {TutorialsSection} from "@/components/TutorialsSection";
@@ -141,6 +142,9 @@ export default function Home() {
{/* ── Agent integration (speak primitive + MCP) ───────────── */}
+ {/* ── Personalities (Compose / Rewrite / Respond) ──────────── */}
+
+
{/* ── API Section ──────────────────────────────────────────── */}
diff --git a/landing/src/components/Navbar.tsx b/landing/src/components/Navbar.tsx
index 565937d2..d292be73 100644
--- a/landing/src/components/Navbar.tsx
+++ b/landing/src/components/Navbar.tsx
@@ -58,7 +58,7 @@ export function Navbar() {
className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
Capture
-
+
New
diff --git a/landing/src/components/Personalities.tsx b/landing/src/components/Personalities.tsx
new file mode 100644
index 00000000..07e2ec3a
--- /dev/null
+++ b/landing/src/components/Personalities.tsx
@@ -0,0 +1,235 @@
+'use client';
+
+import { AnimatePresence, motion } from 'framer-motion';
+import { ArrowRight, MessageSquareReply, PenLine, Sparkles } from 'lucide-react';
+import { useEffect, useState } from 'react';
+
+// ─── Modes ──────────────────────────────────────────────────────────────────
+
+type Mode = {
+ id: 'compose' | 'rewrite' | 'respond';
+ label: string;
+ icon: typeof Sparkles;
+ inputLabel: string;
+ outputLabel: string;
+ input: string;
+ output: string;
+};
+
+const MODES: Mode[] = [
+ {
+ id: 'compose',
+ label: 'Compose',
+ icon: Sparkles,
+ inputLabel: 'Prompt',
+ outputLabel: "Marlowe, in character",
+ input: 'celebrate the deploy going green',
+ output:
+ "She came through clean. Not a single test casting a shadow. In this town, that's usually when you start worrying.",
+ },
+ {
+ id: 'rewrite',
+ label: 'Rewrite',
+ icon: PenLine,
+ inputLabel: 'Your text',
+ outputLabel: "Marlowe, in character",
+ input: 'the build is done and we shipped to production',
+ output:
+ "Build's wrapped, ship's left the dock. Another stack of code makes its way into prod, another row of green checks lining the wall.",
+ },
+ {
+ id: 'respond',
+ label: 'Respond',
+ icon: MessageSquareReply,
+ inputLabel: 'Question',
+ outputLabel: "Marlowe, in character",
+ input: 'should I refactor this before merging or do it after?',
+ output:
+ "Listen, kid. You can polish the brass on the door, or you can open it. Open the door — refactor in daylight.",
+ },
+];
+
+const PERSONA_DESCRIPTION =
+ "1940s noir detective. World-weary, cynical, every situation a metaphor for the city's underbelly. Talks like he's seen one stack trace too many.";
+
+// ─── Persona card ───────────────────────────────────────────────────────────
+
+function PersonaCard() {
+ return (
+
+
+
+
+
Marlowe
+
+ Voice profile · cloned from a 12s sample
+
+
+
+
+
+ Personality
+
+
“{PERSONA_DESCRIPTION}”
+
+ );
+}
+
+// ─── Mode demo ──────────────────────────────────────────────────────────────
+
+function ModeDemo({ mode, cycleKey }: { mode: Mode; cycleKey: number }) {
+ return (
+
+ {/* Mode tabs */}
+
+ {MODES.map((m) => {
+ const Icon = m.icon;
+ const active = m.id === mode.id;
+ return (
+
+
+ {m.label}
+
+ );
+ })}
+
+
+ {/* Input → Output */}
+
+
+
+ {/* Input */}
+
+
+ {mode.inputLabel}
+
+
+ {mode.input}
+
+
+
+ {/* Arrow */}
+
+
+ {/* Output */}
+
+
+ {mode.outputLabel}
+
+
+ “{mode.output}”
+
+
+
+
+
+
+ );
+}
+
+// ─── Bullets ────────────────────────────────────────────────────────────────
+
+const BULLETS = [
+ {
+ icon: Sparkles,
+ title: 'Compose',
+ description:
+ 'Generate a fresh utterance in the character’s voice from a short prompt. Useful for game dialogue, narration cues, or character barks.',
+ },
+ {
+ icon: PenLine,
+ title: 'Rewrite',
+ description:
+ 'Restate your text in their voice while preserving every idea. Same content, their delivery — for scripts, dubs, and consistent character voice across long-form work.',
+ },
+ {
+ icon: MessageSquareReply,
+ title: 'Respond',
+ description:
+ 'Treat your text as a prompt and produce the character’s reply. The persona half of the dictation → speak loop.',
+ },
+];
+
+// ─── Section ────────────────────────────────────────────────────────────────
+
+export function Personalities() {
+ const [idx, setIdx] = useState(0);
+
+ useEffect(() => {
+ const iv = window.setInterval(() => {
+ setIdx((i) => (i + 1) % MODES.length);
+ }, 4500);
+ return () => window.clearInterval(iv);
+ }, []);
+
+ const mode = MODES[idx];
+
+ return (
+
+
+ {/* Header */}
+
+
+ Personalities
+
+
+ Voices with a personality.
+
+
+ Give any voice profile a free-form personality. Then{' '}
+ Compose,{' '}
+ Rewrite, or{' '}
+ Respond — your cloned voice, in full character.
+
+
+
+ {/* Mockup: persona card (left) + mode demo (right) */}
+
+
+ {/* Bullets */}
+
+ {BULLETS.map((bullet) => {
+ const Icon = bullet.icon;
+ return (
+
+
+
+
{bullet.title}
+
+
+ {bullet.description}
+
+
+ );
+ })}
+
+
+
+ );
+}