| name | voicebox-voice-mcp-agent |
| description | Local-first AI voice studio for voice cloning, speech synthesis, dictation, and MCP agent voice integration |
| triggers | ["how do I use voicebox for voice cloning","set up voicebox TTS and STT locally","integrate voicebox with MCP agents","clone a voice with voicebox","configure voicebox dictation hotkey","generate speech with voicebox API","troubleshoot voicebox engine startup","add custom TTS engine to voicebox"] |
Voicebox Voice MCP Agent
Skill by ara.so — MCP Skills collection.
Overview
Voicebox is a local-first AI voice studio that combines voice input (Whisper STT + global dictation) and voice output (7 TTS engines with voice cloning) in a single TypeScript/Rust application. All inference runs on your hardware by default; audio never leaves your machine unless explicitly configured. It provides:
- Seven TTS engines: Qwen3-TTS, Qwen CustomVoice, LuxTTS, Chatterbox Multilingual/Turbo, HumeAI TADA, Kokoro
- Voice cloning: Zero-shot cloning from short reference samples
- Global dictation: System-wide hotkey with paste-into-focused-field
- MCP HTTP server: Agent-driven voice workflows at
/mcp
- 23 languages: Multilingual synthesis
- Audio effects: Pitch, reverb, delay, chorus, compression, filters
- Local LLM refinement: Bundled Qwen3 for transcript cleanup
Stack: Bun monorepo, TypeScript, Tauri 2 (Rust), React 18, Zustand, TanStack Router, Vite
Installation
Prerequisites
curl -fsSL https://bun.sh/install | bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install ffmpeg
sudo apt install ffmpeg
Quick Setup
git clone https://github.com/jamiepine/voicebox.git
cd voicebox
just setup
just dev
Development Modes
bun run dev:web
bun run dev
bun run build
just build:desktop
Configuration
Environment Variables
Create .env in project root:
VOICEBOX_HOST=127.0.0.1
VOICEBOX_PORT=17493
VOICEBOX_CORS_ORIGINS=http://localhost:3000,http://localhost:5173
LOG_LEVEL=info
HF_TOKEN=your_huggingface_token_here
VOICEBOX_REDIS_ENABLED=false
VOICEBOX_REDIS_URL=redis://127.0.0.1:6379
VOICEBOX_REDIS_KEY_PREFIX=voicebox:
MCP Client Configuration
Create .mcp.json to configure MCP client access:
{
"mcpServers": {
"voicebox": {
"url": "http://127.0.0.1:17493/mcp",
"description": "Voicebox voice synthesis and dictation server"
}
}
}
API Usage
REST API
The service exposes a REST API at http://127.0.0.1:17493 when running.
Generate Speech
interface GenerationRequest {
text: string;
profileId: string;
engine?: string;
effects?: AudioEffect[];
}
interface AudioEffect {
type: 'pitch' | 'reverb' | 'delay' | 'chorus' | 'compression';
params: Record<string, number>;
}
async function generateSpeech(text: string, profileId: string): Promise<Blob> {
const response = await fetch('http://127.0.0.1:17493/generations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text,
profileId,
effects: [
{ type: 'pitch', params: { semitones: 2 } },
{ type: 'reverb', params: { roomSize: 0.5, damping: 0.5 } }
]
})
});
return await response.blob();
}
const audioBlob = await generateSpeech(
"Hello, this is a test of voice synthesis.",
"default-profile-id"
);
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
Stream Generation Progress
async function streamGeneration(text: string, profileId: string) {
const response = await fetch('http://127.0.0.1:17493/generations/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, profileId })
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
console.log('Progress:', data.progress, '%');
console.log('Status:', data.status);
}
}
}
}
Voice Cloning
async function cloneVoice(
name: string,
referenceAudio: File,
targetEngine: string = 'qwen-customvoice'
): Promise<string> {
const formData = new FormData();
formData.append('name', name);
formData.append('reference', referenceAudio);
formData.append('engine', targetEngine);
const response = await fetch('http://127.0.0.1:17493/voices/clone', {
method: 'POST',
body: formData
});
const { voiceId } = await response.json();
return voiceId;
}
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const audioFile = fileInput.files![0];
const voiceId = await cloneVoice('My Custom Voice', audioFile);
console.log('Voice cloned with ID:', voiceId);
Service Layer (Internal)
When extending Voicebox or adding features:
export interface TTSEngine {
name: string;
synthesize(text: string, options: SynthesisOptions): Promise<AudioBuffer>;
clone?(reference: AudioBuffer): Promise<VoiceModel>;
supportedLanguages: string[];
}
export interface SynthesisOptions {
voiceId?: string;
language?: string;
speed?: number;
pitch?: number;
}
import type { TTSEngine, SynthesisOptions } from './types';
export class CustomTTSEngine implements TTSEngine {
name = 'custom-tts';
supportedLanguages = ['en', 'es', 'fr'];
async synthesize(text: string, options: SynthesisOptions): Promise<AudioBuffer> {
const response = await fetch('http://your-tts-service/synthesize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text,
voice_id: options.voiceId,
language: options.language || 'en',
speed: options.speed || 1.0,
pitch: options.pitch || 0
})
});
const arrayBuffer = await response.arrayBuffer();
const audioContext = new AudioContext();
return await audioContext.decodeAudioData(arrayBuffer);
}
async clone(reference: AudioBuffer): Promise<VoiceModel> {
throw new Error('Cloning not implemented for this engine');
}
}
import { CustomTTSEngine } from './engines/custom';
export const ttsEngines: TTSEngine[] = [
new CustomTTSEngine(),
];
Platform Adapters
Voicebox uses platform adapters to abstract native capabilities:
export interface Platform {
readFile(path: string): Promise<Uint8Array>;
writeFile(path: string, data: Uint8Array): Promise<void>;
startAudioCapture(): Promise<MediaStream>;
stopAudioCapture(): void;
registerHotkey?(combo: string, callback: () => void): Promise<void>;
unregisterHotkey?(combo: string): Promise<void>;
checkForUpdates?(): Promise<UpdateInfo | null>;
installUpdate?(): Promise<void>;
}
import { invoke } from '@tauri-apps/api/core';
import { register, unregister } from '@tauri-apps/plugin-global-shortcut';
export const desktopPlatform: Platform = {
async readFile(path: string): Promise<Uint8Array> {
return await invoke('read_file', { path });
},
async writeFile(path: string, data: Uint8Array): Promise<void> {
await invoke('write_file', { path, data: Array.from(data) });
},
async startAudioCapture(): Promise<MediaStream> {
return await navigator.mediaDevices.getUserMedia({ audio: true });
},
stopAudioCapture(): void {
},
async registerHotkey(combo: string, callback: () => void): Promise<void> {
await register(combo, callback);
},
async unregisterHotkey(combo: string): Promise<void> {
await unregister(combo);
}
};
MCP Integration
MCP Server Endpoints
The MCP server runs at http://127.0.0.1:17493/mcp and exposes tools for agent-driven workflows:
{
"tools": [
{
"name": "synthesize_speech",
"description": "Generate speech audio from text using a specific voice profile",
"inputSchema": {
"type": "object",
"properties": {
"text": { "type": "string", "description": "Text to synthesize" },
"profile_id": { "type": "string", "description": "Voice profile ID" },
"engine": { "type": "string", "description": "Optional TTS engine override" }
},
"required": ["text", "profile_id"]
}
},
{
"name": "clone_voice",
"description": "Clone a voice from reference audio",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Name for the cloned voice" },
"reference_path": { "type": "string", "description": "Path to reference audio" },
"engine": { "type": "string", "description": "TTS engine to use for cloning" }
},
"required": ["name", "reference_path"]
}
},
{
"name": "start_dictation",
"description": "Start voice dictation and paste into focused field",
"inputSchema": {
"type": "object",
"properties": {
"language": { "type": "string", "description": "Language code (e.g., 'en', 'es')" }
}
}
}
]
}
MCP Client Usage
interface MCPRequest {
tool: string;
arguments: Record<string, unknown>;
}
async function callMCPTool(tool: string, args: Record<string, unknown>) {
const response = await fetch('http://127.0.0.1:17493/mcp/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool, arguments: args })
});
return await response.json();
}
const result = await callMCPTool('synthesize_speech', {
text: 'Hello from the MCP agent!',
profile_id: 'default-voice',
engine: 'qwen3-tts'
});
console.log('Audio URL:', result.audioUrl);
State Management
Voicebox uses Zustand for state management:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface VoiceProfile {
id: string;
name: string;
engine: string;
voiceId: string;
settings: Record<string, unknown>;
}
interface VoiceStore {
profiles: VoiceProfile[];
activeProfileId: string | null;
addProfile: (profile: VoiceProfile) => void;
removeProfile: (id: string) => void;
setActiveProfile: (id: string) => void;
updateProfile: (id: string, updates: Partial<VoiceProfile>) => void;
}
export const useVoiceStore = create<VoiceStore>()(
persist(
(set) => ({
profiles: [],
activeProfileId: null,
addProfile: (profile) =>
set((state) => ({
profiles: [...state.profiles, profile]
})),
removeProfile: (id) =>
set((state) => ({
profiles: state.profiles.filter((p) => p.id !== id),
activeProfileId: state.activeProfileId === id ? null : state.activeProfileId
})),
setActiveProfile: (id) =>
set({ activeProfileId: id }),
updateProfile: (id, updates) =>
set((state) => ({
profiles: state.profiles.map((p) =>
p.id === id ? { ...p, ...updates } : p
)
}))
}),
{ name: 'voice-profiles' }
)
);
import { useVoiceStore } from './stores/voice';
function VoiceSelector() {
const { profiles, activeProfileId, setActiveProfile } = useVoiceStore();
return (
<select
value={activeProfileId || ''}
onChange={(e) => setActiveProfile(e.target.value)}
>
{profiles.map((profile) => (
<option key={profile.id} value={profile.id}>
{profile.name}
</option>
))}
</select>
);
}
Common Patterns
Voice Generation with Effects
import { useState } from 'react';
import { useVoiceStore } from '../stores/voice';
export function SpeechGenerator() {
const [text, setText] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const activeProfileId = useVoiceStore((s) => s.activeProfileId);
async function handleGenerate() {
if (!activeProfileId || !text) return;
setIsGenerating(true);
try {
const response = await fetch('http://127.0.0.1:17493/generations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text,
profileId: activeProfileId,
effects: [
{ type: 'pitch', params: { semitones: 0 } },
{ type: 'reverb', params: { roomSize: 0.3, damping: 0.4 } }
]
})
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
setAudioUrl(url);
} catch (error) {
console.error('Generation failed:', error);
} finally {
setIsGenerating(false);
}
}
return (
<div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Enter text to synthesize..."
rows={5}
/>
<button onClick={handleGenerate} disabled={isGenerating}>
{isGenerating ? 'Generating...' : 'Generate Speech'}
</button>
{audioUrl && (
<audio controls src={audioUrl}>
Your browser does not support the audio element.
</audio>
)}
</div>
);
}
Global Dictation Hotkey (Desktop)
import { register } from '@tauri-apps/plugin-global-shortcut';
import { invoke } from '@tauri-apps/api/core';
export async function setupDictationHotkey(combo: string = 'CommandOrControl+Shift+D') {
await register(combo, async () => {
console.log('Dictation hotkey triggered');
await invoke('start_dictation');
});
}
#[tauri::command]
async fn start_dictation() -> Result<(), String> {
Ok(())
}
Voice Cloning Workflow
import { useState } from 'react';
export function VoiceCloner() {
const [name, setName] = useState('');
const [file, setFile] = useState<File | null>(null);
const [isCloning, setIsCloning] = useState(false);
const [result, setResult] = useState<string | null>(null);
async function handleClone() {
if (!name || !file) return;
setIsCloning(true);
try {
const formData = new FormData();
formData.append('name', name);
formData.append('reference', file);
formData.append('engine', 'qwen-customvoice');
const response = await fetch('http://127.0.0.1:17493/voices/clone', {
method: 'POST',
body: formData
});
const { voiceId } = await response.json();
setResult(`Voice cloned successfully! ID: ${voiceId}`);
} catch (error) {
console.error('Cloning failed:', error);
setResult('Cloning failed. Check console for details.');
} finally {
setIsCloning(false);
}
}
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Voice name"
/>
<input
type="file"
accept="audio/*"
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
<button onClick={handleClone} disabled={isCloning || !name || !file}>
{isCloning ? 'Cloning...' : 'Clone Voice'}
</button>
{result && <p>{result}</p>}
</div>
);
}
Testing
TypeScript Unit Tests
bun run test
bun run test -- --coverage
bun test src/redis/cache.test.ts
Example test structure:
import { describe, it, expect, beforeEach } from 'vitest';
import { TTSEngineRegistry } from './registry';
import { MockTTSEngine } from './mocks';
describe('TTS Engine Registry', () => {
let registry: TTSEngineRegistry;
beforeEach(() => {
registry = new TTSEngineRegistry();
registry.register(new MockTTSEngine());
});
it('should register and retrieve engines', () => {
const engine = registry.get('mock-tts');
expect(engine).toBeDefined();
expect(engine?.name).toBe('mock-tts');
});
it('should synthesize text', async () => {
const engine = registry.get('mock-tts')!;
const buffer = await engine.synthesize('Hello world', {
language: 'en',
speed: 1.0
});
expect(buffer).toBeInstanceOf(AudioBuffer);
});
});
Rust Tests
cd tauri/src-tauri
cargo test
cargo test -- --nocapture
End-to-End Checks
bun run ci
Troubleshooting
Service Won't Start
Problem: API service fails to start or port 17493 is in use.
Solutions:
lsof -i :17493
netstat -ano | findstr :17493
kill -9 <PID>
export VOICEBOX_PORT=8080
bun run dev:web
curl http://127.0.0.1:17493/health
Missing Sidecar Binaries (Tauri)
Problem: bun run dev fails with missing sidecar error.
Solution:
bun run setup:dev
bun run build:sidecar
bun run dev
TTS Engine Failures
Problem: Speech generation fails or returns empty audio.
Solutions:
const engines = await fetch('http://127.0.0.1:17493/engines').then(r => r.json());
console.log('Available engines:', engines);
const profiles = await fetch('http://127.0.0.1:17493/profiles').then(r => r.json());
console.log('Voice profiles:', profiles);
const response = await fetch('http://127.0.0.1:17493/generations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: 'Test',
profileId: 'default-profile-id',
engine: 'kokoro'
})
});
if (!response.ok) {
const error = await response.text();
console.error('Generation error:', error);
}
Model Download Failures
Problem: HuggingFace model downloads fail or timeout.
Solutions:
export HF_TOKEN=your_token_here
df -h
Get-PSDrive
huggingface-cli download model-name --local-dir ./models
export HTTP_PROXY=http://proxy:port
export HTTPS_PROXY=http://proxy:port
Redis Persistence Errors (Web Deployments)
Problem: Connection errors when persistence cache is enabled.
Solutions:
export VOICEBOX_REDIS_ENABLED=false
docker run --rm -p 6379:6379 redis:7-alpine
redis-cli -h 127.0.0.1 -p 6379 ping
export VOICEBOX_REDIS_URL=redis://127.0.0.1:6379
bun run bootstrap:persistence
TypeScript Build Errors
Problem: Type errors across workspaces.
Solutions:
bun run typecheck
cd app && bun run typecheck
cd tauri && bun run typecheck
rm -rf node_modules bun.lockb
bun install
bun run build:types
Audio Capture Issues (Desktop)
Problem: Microphone access denied or no audio captured.
Solutions:
- Grant microphone permissions in system settings
- Check browser/app permissions
- Test with different audio input device:
const devices = await navigator.mediaDevices.enumerateDevices();
const audioInputs = devices.filter(d => d.kind === 'audioinput');
console.log('Available microphones:', audioInputs);
const stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: audioInputs[0].deviceId } }
});
GPU Acceleration Issues
Problem: TTS inference is slow or not using GPU.
Solutions:
nvidia-smi
python3 -c "import mlx; print(mlx.core.metal.is_available())"
export VOICEBOX_FORCE_CPU=true
export VOICEBOX_MAX_BATCH_SIZE=1
Project Commands Reference
just setup
bun install
bun run setup:dev
just dev
bun run dev:web
bun run dev
bun run build
bun run build:web
just build:desktop
bun run test
bun run typecheck
bun run check
bun run ci
just check
bun run preview
docker-compose up
curl http://127.0.0.1:17493/health
just clean
Key Files
app/src/services/tts/ — TTS engine implementations
app/src/services/stt/ — Speech-to-text (Whisper)
app/src/stores/ — Zustand state management
tauri/src/platform/ — Native capability implementations
src/redis/ — Persistence cache layer
.env.redis.example — Redis configuration template
justfile — Primary task orchestration
biome.json — Lint and format configuration