Merge pull request #22 from boudra/local-streaming-speech-tts

voice: harden local voice agent path and UUID validation
This commit is contained in:
Mohamed Boudra
2026-02-07 00:31:30 +07:00
committed by GitHub
69 changed files with 3991 additions and 2096 deletions
+21
View File
@@ -13,6 +13,7 @@ import { Route as DocsRouteImport } from './routes/docs'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsWorktreesRouteImport } from './routes/docs/worktrees'
import { Route as DocsVoiceRouteImport } from './routes/docs/voice'
import { Route as DocsSecurityRouteImport } from './routes/docs/security'
import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration'
import { Route as DocsCliRouteImport } from './routes/docs/cli'
@@ -38,6 +39,11 @@ const DocsWorktreesRoute = DocsWorktreesRouteImport.update({
path: '/worktrees',
getParentRoute: () => DocsRoute,
} as any)
const DocsVoiceRoute = DocsVoiceRouteImport.update({
id: '/voice',
path: '/voice',
getParentRoute: () => DocsRoute,
} as any)
const DocsSecurityRoute = DocsSecurityRouteImport.update({
id: '/security',
path: '/security',
@@ -66,6 +72,7 @@ export interface FileRoutesByFullPath {
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/docs/': typeof DocsIndexRoute
}
@@ -75,6 +82,7 @@ export interface FileRoutesByTo {
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/docs': typeof DocsIndexRoute
}
@@ -86,6 +94,7 @@ export interface FileRoutesById {
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/docs/': typeof DocsIndexRoute
}
@@ -98,6 +107,7 @@ export interface FileRouteTypes {
| '/docs/cli'
| '/docs/configuration'
| '/docs/security'
| '/docs/voice'
| '/docs/worktrees'
| '/docs/'
fileRoutesByTo: FileRoutesByTo
@@ -107,6 +117,7 @@ export interface FileRouteTypes {
| '/docs/cli'
| '/docs/configuration'
| '/docs/security'
| '/docs/voice'
| '/docs/worktrees'
| '/docs'
id:
@@ -117,6 +128,7 @@ export interface FileRouteTypes {
| '/docs/cli'
| '/docs/configuration'
| '/docs/security'
| '/docs/voice'
| '/docs/worktrees'
| '/docs/'
fileRoutesById: FileRoutesById
@@ -156,6 +168,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsWorktreesRouteImport
parentRoute: typeof DocsRoute
}
'/docs/voice': {
id: '/docs/voice'
path: '/voice'
fullPath: '/docs/voice'
preLoaderRoute: typeof DocsVoiceRouteImport
parentRoute: typeof DocsRoute
}
'/docs/security': {
id: '/docs/security'
path: '/security'
@@ -192,6 +211,7 @@ interface DocsRouteChildren {
DocsCliRoute: typeof DocsCliRoute
DocsConfigurationRoute: typeof DocsConfigurationRoute
DocsSecurityRoute: typeof DocsSecurityRoute
DocsVoiceRoute: typeof DocsVoiceRoute
DocsWorktreesRoute: typeof DocsWorktreesRoute
DocsIndexRoute: typeof DocsIndexRoute
}
@@ -201,6 +221,7 @@ const DocsRouteChildren: DocsRouteChildren = {
DocsCliRoute: DocsCliRoute,
DocsConfigurationRoute: DocsConfigurationRoute,
DocsSecurityRoute: DocsSecurityRoute,
DocsVoiceRoute: DocsVoiceRoute,
DocsWorktreesRoute: DocsWorktreesRoute,
DocsIndexRoute: DocsIndexRoute,
}
+1
View File
@@ -7,6 +7,7 @@ export const Route = createFileRoute('/docs')({
const navigation = [
{ name: 'Getting started', href: '/docs' },
{ name: 'Voice', href: '/docs/voice' },
{ name: 'Git worktrees', href: '/docs/worktrees' },
{ name: 'CLI', href: '/docs/cli' },
{ name: 'Configuration', href: '/docs/configuration' },
@@ -67,8 +67,7 @@ function Configuration() {
"$schema": "https://paseo.sh/schemas/paseo.config.v1.json",
"version": 1,
"providers": {
"openai": { "apiKey": "..." },
"openrouter": { "apiKey": "..." }
"openai": { "apiKey": "..." }
},
"daemon": {
"listen": "127.0.0.1:6767",
@@ -79,13 +78,32 @@ function Configuration() {
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Voice</h2>
<p className="text-white/60 leading-relaxed">
Voice is configured through <code className="font-mono">features.dictation</code> and{' '}
<code className="font-mono">features.voiceMode</code>, with provider credentials under{' '}
<code className="font-mono">providers</code>.
</p>
<p className="text-white/60 leading-relaxed">
For voice philosophy, architecture, and complete local/OpenAI setup examples, see{' '}
<a href="/docs/voice" className="underline hover:text-white/80">Voice docs</a>.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Common env vars</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li><code className="font-mono">PASEO_HOME</code> — set Paseo home directory</li>
<li><code className="font-mono">PASEO_LISTEN</code> — override <code className="font-mono">daemon.listen</code></li>
<li><code className="font-mono">PASEO_ALLOWED_HOSTS</code> — override/extend <code className="font-mono">daemon.allowedHosts</code></li>
<li><code className="font-mono">OPENAI_API_KEY</code> and <code className="font-mono">OPENROUTER_API_KEY</code> — override provider keys</li>
<li><code className="font-mono">OPENAI_API_KEY</code> — override OpenAI provider key</li>
<li><code className="font-mono">PASEO_VOICE_LLM_PROVIDER</code> — override voice LLM provider (<code className="font-mono">claude</code>, <code className="font-mono">codex</code>, <code className="font-mono">opencode</code>)</li>
<li><code className="font-mono">PASEO_DICTATION_STT_PROVIDER</code>, <code className="font-mono">PASEO_VOICE_STT_PROVIDER</code>, <code className="font-mono">PASEO_VOICE_TTS_PROVIDER</code> — override voice provider selection (<code className="font-mono">local</code> or <code className="font-mono">openai</code>)</li>
<li><code className="font-mono">PASEO_LOCAL_MODELS_DIR</code> and <code className="font-mono">PASEO_LOCAL_AUTO_DOWNLOAD</code> — control local model directory and download behavior</li>
<li><code className="font-mono">PASEO_DICTATION_LOCAL_STT_MODEL</code> — override local dictation STT model</li>
<li><code className="font-mono">PASEO_VOICE_LOCAL_STT_MODEL</code>, <code className="font-mono">PASEO_VOICE_LOCAL_TTS_MODEL</code> — override local voice STT/TTS models</li>
<li><code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEAKER_ID</code>, <code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEED</code> — optional local voice TTS tuning</li>
</ul>
</section>
@@ -101,4 +119,3 @@ function Configuration() {
</div>
)
}
+11 -6
View File
@@ -87,20 +87,25 @@ function GettingStarted() {
<section className="space-y-4">
<h2 className="text-xl font-medium">Voice Setup</h2>
<p className="text-white/60">
Voice features currently require an OpenAI API key. Set it as an environment variable before running the server:
Paseo includes first-class voice support with a local-first architecture and configurable speech
providers.
</p>
<div className="bg-card border border-border rounded-lg p-4 font-mono text-sm">
<span className="text-muted-foreground select-none">$ </span>
<span>export OPENAI_API_KEY=your-key-here</span>
</div>
<p className="text-white/60">
Local voice support is coming soon.
For architecture, local model behavior, and provider configuration, see the Voice docs page.
</p>
<a href="/docs/voice" className="underline hover:text-white/80">
Voice docs
</a>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Next</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>
<a href="/docs/voice" className="underline hover:text-white/80">
Voice
</a>
</li>
<li>
<a href="/docs/configuration" className="underline hover:text-white/80">
Configuration
+126
View File
@@ -0,0 +1,126 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/docs/voice')({
head: () => ({
meta: [
{ title: 'Voice - Paseo Docs' },
{
name: 'description',
content: 'Paseo voice architecture, local-first model execution, and provider configuration.',
},
],
}),
component: VoiceDocs,
})
function VoiceDocs() {
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-medium font-title mb-4">Voice</h1>
<p className="text-white/60 leading-relaxed">
Paseo has first-class voice support for dictation and realtime conversations with your coding
environment.
</p>
</div>
<section className="space-y-4">
<h2 className="text-xl font-medium">Philosophy</h2>
<p className="text-white/60 leading-relaxed">
Voice is local-first. You can run speech fully on-device, or choose OpenAI for speech features.
For voice reasoning/orchestration, Paseo reuses agent providers already installed and authenticated
on your machine.
</p>
<p className="text-white/60 leading-relaxed">
This keeps credentials and execution in your environment and avoids introducing a separate
cloud-only voice stack.
</p>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Architecture</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li>Speech I/O: STT and TTS providers per feature (<code className="font-mono">local</code> or <code className="font-mono">openai</code>)</li>
<li>Local speech runtime: ONNX models executed on CPU by default</li>
<li>Voice LLM orchestration: hidden agent session using your configured provider (<code className="font-mono">claude</code>, <code className="font-mono">codex</code>, or <code className="font-mono">opencode</code>)</li>
<li>Tooling path: MCP stdio bridge for voice tools and agent control</li>
</ul>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Local Speech</h2>
<p className="text-white/60 leading-relaxed">
Local speech defaults to model IDs <code className="font-mono">parakeet-tdt-0.6b-v3-int8</code>{' '}
(STT) and <code className="font-mono">pocket-tts-onnx-int8</code> (TTS).
</p>
<p className="text-white/60 leading-relaxed">
Missing models are downloaded at daemon startup into{' '}
<code className="font-mono">$PASEO_HOME/models/local-speech</code> when auto-download is enabled.
Downloads happen only for missing files.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"version": 1,
"features": {
"dictation": { "stt": { "provider": "local", "model": "parakeet-tdt-0.6b-v3-int8" } },
"voiceMode": {
"llm": { "provider": "claude", "model": "haiku" },
"stt": { "provider": "local", "model": "parakeet-tdt-0.6b-v3-int8" },
"tts": { "provider": "local", "model": "pocket-tts-onnx-int8" }
}
},
"providers": {
"local": {
"modelsDir": "~/.paseo/models/local-speech",
"autoDownload": true
}
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">OpenAI Speech Option</h2>
<p className="text-white/60 leading-relaxed">
You can switch dictation, voice STT, and voice TTS to OpenAI by setting provider fields to{' '}
<code className="font-mono">openai</code> and providing <code className="font-mono">OPENAI_API_KEY</code>.
</p>
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
{`{
"version": 1,
"features": {
"dictation": { "stt": { "provider": "openai" } },
"voiceMode": {
"stt": { "provider": "openai" },
"tts": { "provider": "openai" }
}
},
"providers": {
"openai": { "apiKey": "..." }
}
}`}
</pre>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Environment Variables</h2>
<ul className="text-white/60 space-y-2 list-disc list-inside">
<li><code className="font-mono">OPENAI_API_KEY</code> — OpenAI speech credentials</li>
<li><code className="font-mono">PASEO_VOICE_LLM_PROVIDER</code> — voice agent provider override</li>
<li><code className="font-mono">PASEO_LOCAL_MODELS_DIR</code>, <code className="font-mono">PASEO_LOCAL_AUTO_DOWNLOAD</code> — local model storage and download policy</li>
<li><code className="font-mono">PASEO_DICTATION_LOCAL_STT_MODEL</code> — local dictation STT model ID</li>
<li><code className="font-mono">PASEO_VOICE_LOCAL_STT_MODEL</code>, <code className="font-mono">PASEO_VOICE_LOCAL_TTS_MODEL</code> — local voice STT/TTS model IDs</li>
<li><code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEAKER_ID</code>, <code className="font-mono">PASEO_VOICE_LOCAL_TTS_SPEED</code> — optional local voice TTS tuning</li>
</ul>
</section>
<section className="space-y-4">
<h2 className="text-xl font-medium">Operational Notes</h2>
<p className="text-white/60 leading-relaxed">
Realtime voice can launch and control agents. Treat voice prompts with the same care as direct
agent instructions, especially when specifying working directories or destructive operations.
</p>
</section>
</div>
)
}
+2 -2
View File
@@ -286,8 +286,8 @@ function FAQ() {
<FAQItem question="Is this free?">
Paseo is free and open source. It wraps CLI tools like Claude Code and
Codex, which you'll need to have installed and configured with your
own credentials. Voice features currently require an OpenAI API key,
but local voice is coming soon.
own credentials. Voice is local-first by default and can optionally use
OpenAI speech providers if you configure them.
</FAQItem>
<FAQItem question="Does my code leave my machine?">
Paseo itself doesn't send your code anywhere. Agents run locally and