- /llm/generate's "model is downloading" branch was raising HTTPException(202, detail={...}), which wraps the payload in {"detail": ...} and forces clients to parse a success status as if it were an error. Switched to JSONResponse so the payload sits at the top level.
- The landing page's "Language Models" card advertised "Qwen 3.5" with sizes 4B/2B/0.8B; we ship Qwen3 at 0.6B/1.7B/4B. Aligned to what's actually in the binary.
- ProfileForm's discard-draft button reset the form without touching `personality` or `avatarFile`, so stale persona text and an attached avatar would survive the discard. The other three resets in the file already include both fields — this brings the discard path in line.
Routes were calling model_dump(exclude_none=True), which drops every
client-sent null before it reaches the service. The service then layered
on its own `if value is not None` guard. Net effect: setting a nullable
column back to null was a no-op — the MCPPage default-voice picker sends
null when the user picks "no default" and the row was silently keeping
whatever was there before.
Switched the routes to exclude_unset=True so absent fields stay absent
but explicit nulls survive the dump, and centralised the per-field
nullability check in the service. The check inspects the SQLAlchemy
column metadata so non-nullable columns (stt_model, llm_model, the chord
key lists) still drop nulls instead of crashing the request, while
default_playback_voice_id can finally be cleared.
- Hand /generate ids to the global SSE watcher so playback fires on completion. The mutation onSuccess was checking audio_path on a queued row, which is always empty — autoplay never ran.
- Bind the Play As voice selection to capture_settings.default_playback_voice_id, kept in sync with the Settings → Captures and Settings → MCP pickers. Picking from the split-button dropdown writes back to settings.
- Extract AudioBars from HistoryTable into a shared component; use it for the Play As generating state in place of Loader2.
- Stop the active-state hover from flashing white text when the button is in its lighter accent/10 fill.
- Drop the gradient avatar swatches from the Settings → Captures voice dropdown.
- Backend: when the gen worker exits without writing a terminal status (e.g. SQLite lock racing the failed-status write inside its own exception handler), the cancel endpoint now flips the row to failed instead of 409-ing. Worker also force-fails on its way out as a belt-and-suspenders.
ListPane is a compound component (Header / TitleRow / Title / Actions /
Search / Scroll) that owns the relative wrapper, faded right divider
(50px top fade), top scroll mask, and absolute-positioned header used by
every list-detail tab. Wires up CapturesTab, StoryList, and EffectsList.
EffectsTab gets -mx-8 / pr-8 to match the edge-to-edge layout used
elsewhere.
Other changes:
- MCPPage: native <select> → shadcn <Select> for default voice and
per-binding voice pickers
- Button outline variant: add hover:border-accent
- Drop hover:text-destructive from trailing delete buttons
(HistoryTable, GpuAcceleration, GpuPage, EffectsChainEditor,
EffectsDetail)
- HistoryTable empty state moved behind t('history.empty')
- StoryContent scroll padding pt-14 → pt-16
- backend health reports the captures dir
- landing CapturesMockup: "Send to" → "Export" with Download icon
- CHANGELOG: drop [Unreleased] personality section
- 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
});
```
Stops the "stuck pill" failure where pressing the chord with missing
STT/LLM models triggers a recording that has nowhere to land. The
hotkey now stays disarmed until every gate (models downloaded, Input
Monitoring + Accessibility granted) is green; the empty-state checklist
in CapturesTab surfaces each unmet gate with a one-click action and
auto-arms the chord once everything turns green.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The pill window now surfaces for agent-initiated speech without main-window
involvement. Rust subscribes to /events/speak via a tokio task + reqwest
streaming body (speak_monitor.rs), shows the pill, and forwards events to
the dictate webview over Tauri's event bus. The pill plays audio via a
plain HTMLAudioElement and emits dictate:hide when playback ends. The
pill stays hidden through the ~1 s generation wait and only surfaces when
audio actually starts, with the counter armed at that moment.
Fixes a shared-dict mutation in mcp_server/events.publish() that caused
the second subscriber (Rust speak_monitor) to receive `event: message`
instead of named speak-start/speak-end frames. Also teaches the speak_monitor
parser to handle CRLF framing (sse-starlette default). Main-window
AudioPlayer now skips autoplay for source in {mcp, rest} to avoid
double-play when both windows are alive.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Ships the Capture release end to end. Global-hotkey dictation with
synthetic paste into the focused app on macOS and Windows, an on-screen
pill across recording / transcribing / refining, customizable push-to-
talk and toggle chords, and an accessibility-permission prompt scoped to
Settings → Captures with inline re-check feedback.
Voice profiles gain optional personalities that power compose / rewrite /
respond actions via a local Qwen3 LLM — shared with refinement, so there
is one local LLM in the app, not two.
Refinement hardened with deterministic Whisper-loop collapse before the
LLM sees the transcript, per-capture flag snapshots for re-runs, and a
ten-transcript evaluation harness across every bundled refinement size.
Version bump 0.4.5 → 0.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
When migrating model path with an empty cache, backend returned early without emitting migration completion SSE, causing frontend overlay to hang. This patch emits complete status for empty migrations.
- Show all voice profiles with unsupported ones grayed out (opacity) instead of hidden
- Clicking a grayed-out profile selects it and auto-switches the engine to a compatible one
- Sort supported profiles first, with info tip about compatibility at the bottom
- Scroll to selected profile after engine/sort changes with safe margin
- Fix engine desync on tab navigation by initializing form engine from store
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When the model wasn't loaded, the app was closed mid-run, or a
generation otherwise errored out, the resulting "Failed" rows
accumulate in history and there was no way to remove them in bulk —
individual delete was the only option.
Adds a header row above the history list (only rendered when at
least one failed generation is present) with a "Clear failed" button
that opens a confirmation dialog, then calls a new
DELETE /history/failed endpoint which sweeps all status='failed'
rows (plus their version files / audio files on disk).
Closesjamiepine/voicebox#410
Co-authored-by: Claude Opus 4.6 <[email protected]>
Applies the compatibility-checker portion of #367. Adds a
check_cuda_compatibility() helper that compares the current device's
compute capability against torch.cuda._get_arch_list() and returns a
human-readable warning if the PyTorch build doesn't support it.
Wired into three places:
• HealthResponse gains a gpu_compatibility_warning field so clients
can surface the issue in the UI
• Startup logs the warning as WARN level
• _get_gpu_status() appends "[UNSUPPORTED - see logs]" to the GPU
label shown in settings
Skipped #367's other half — the switch from stable to nightly cu128
wheels across release.yml, build_binary.py, and justfile. That's
redundant with #401's TORCH_CUDA_ARCH_LIST=...12.0+PTX approach and
would introduce non-deterministic builds from shifting nightly
releases.
Co-Authored-By: nyzxor <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(history): populate status/error/engine/model_size/is_favorited from DB
GET /history/{generation_id} was constructing HistoryResponse without
passing status, error, engine, model_size, or is_favorited from the
DB row. Since HistoryResponse.status defaults to "completed" in the
Pydantic model (models.py:141), this endpoint returned
status="completed" for every generation regardless of the actual DB
state — including jobs still in "loading_model" or "generating", and
even "failed" jobs.
This breaks any client polling /history/{id} for job completion:
the API lies about the status, so the only trustworthy success
signal becomes `audio_path` being non-empty. All other fields left
at their model defaults were similarly masked.
Fix: pass all fields through from the DB row, matching the pattern
used elsewhere in the codebase. The DB model (Generation in
database/models.py) already has all these columns.
* fix(history): apply NULL fallbacks to match list endpoint
Align the defensive mappings with services/history.py:206-223 so
both the single-item and list history endpoints handle legacy rows
with NULL status/engine/is_favorited identically. Without this,
HistoryResponse's non-Optional str/bool fields would raise a
pydantic ValidationError (500) on any row where these columns are
NULL — possible from direct SQL updates or past migrations.
Addresses review feedback on PR #394.
---------
Co-authored-by: malletfils <[email protected]>
Add Kokoro-82M as a new TTS engine — 82M params, CPU realtime, 8 languages,
Apache 2.0. Unlike cloning engines, Kokoro uses pre-built voice styles, which
required a new profile type system to support non-cloning engines cleanly.
Kokoro engine:
- New kokoro_backend.py implementing TTSBackend protocol
- 50 built-in voices across en/es/fr/hi/it/pt/ja/zh
- KPipeline API with language-aware G2P routing via misaki
- PyInstaller bundling for misaki, language_tags, espeakng_loader, en_core_web_sm
Voice profile type system:
- New voice_type column: 'cloned' | 'preset' | 'designed' (future)
- Preset profiles store engine + voice ID instead of audio samples
- default_engine field on profiles — auto-selects engine on profile pick
- Create Voice dialog: toggle between 'Clone from audio' and 'Built-in voice'
- Edit dialog shows preset voice info instead of sample list for preset profiles
- Engine selector locks to preset engine when preset profile is selected
- Profile grid filters by engine — shows Kokoro voices when Kokoro selected
- Custom empty state when no preset profiles exist for selected engine
Bug fixes:
- Fix relative audio paths in DB causing 404s in production builds
- config.set_data_dir() now resolves to absolute paths
- Startup migration converts existing relative paths to absolute
Also updates PROJECT_STATUS.md and tts-engines.mdx developer guide.
Address CodeRabbit review feedback and user-reported GPU acceleration failure:
- Use shared manual_seed() in chatterbox, chatterbox_turbo, and luxtts
backends so XPU (and future accelerators) get proper device seeding
- Add XPU branch to _get_gpu_status() so startup log reports Intel Arc
GPUs instead of 'None (CPU only)'
- Add XPU VRAM reporting and correct backend_variant fallback in the
/health endpoint
- Switch justfile GPU detection from Get-WmiObject to Get-CimInstance,
simplify the Arc regex to match 'Arc' (not 'Intel.*Arc'), log
detected GPUs, and print manual install instructions on miss
Resolves the root cause where IPEX was silently not installed due to
WMI detection failure, causing CPU-only fallback on Intel Arc systems.
Move audio validation and saving to thread pool so librosa/ffmpeg decoding
doesn't block the async event loop. Combine validate + load into a single
pass to avoid decoding the file twice. Add 50 MB upload limit and chunked
reads to prevent unbounded memory allocation.
Closes#278
Wrap SSE generators with BrokenPipeError/ConnectionResetError handling
so client disconnects during generation status polling, download progress,
or audio streaming don't produce unhandled Errno 32 errors.
Closes#248
Critical:
- Remove dead backend.utils.validation PyInstaller hidden import
- Fix story_items table rebuild to preserve track/trim/version columns
- Guard cache migration against same source/destination path
- Fix regeneration audio overwrite (use random uuid suffix per take)
Major:
- Engine selector: validate language on Qwen switch, clear stale modelSize
- Sync language validation regex between profile create and generate (22 langs)
- Guard CUDA download against duplicate concurrent requests
- Only set model_size for engines that support multiple sizes
- Fix 404 swallowed by generic except in history export
- Validate audio_path before FileResponse in export-audio
- Transcription: stream uploads in 1MB chunks, use robust cache check,
call complete_download() on Whisper download success
- Set clean version as default when effects chain validation fails
- Return explicit error when Windows port occupied by non-voicebox process
Split the 2,578-line main.py (90 routes) into 12 domain-specific router
modules under routes/. main.py is now a 45-line entry point.
New structure:
- app.py: FastAPI instance, CORS, startup/shutdown, safe_content_disposition
- routes/: health, profiles, channels, generations, history, transcription,
stories, effects, audio, models, tasks, cuda
- services/cuda.py: moved from cuda_download.py
Also includes Phase 5 database/ package (from parallel agent):
- database/__init__.py re-exports all symbols for backward compat
- database/models.py, session.py, migrations.py, seed.py
All 90 routes verified registered and app imports cleanly.