* Fix voice sample validation on Python 3.13
Python 3.13 removed audioop from the standard library, which broke reference
audio validation when adding voice samples. Add the audioop-lts backport for
3.13+ installs and bundle audioop in PyInstaller builds on the same versions.
* style(tests): satisfy Ruff import ordering
---------
Co-authored-by: Jamie Pine <[email protected]>
* fix(backend): return 404 instead of 500 for audio of failed generations
A failed generation stores an empty audio_path. resolve_storage_path("")
resolved to the data directory itself, which exists, so the route's 404
guard passed and FileResponse raised RuntimeError ("File at path .../data
is not a file"), surfacing as a 500.
- resolve_storage_path now returns None for empty paths
- audio routes check is_file() instead of exists() so directories never
reach FileResponse
- GET /audio/{generation_id} reports "Generation failed; no audio
available" when the generation status is failed
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(backend): reject empty Path objects in resolve_storage_path
Path("") is truthy, so the previous `if not path` guard only caught
None and empty strings. Callers such as database/migrations.py pass
Path objects, so an empty Path could still resolve to the data dir.
Check None separately and reject paths with no parts.
Also add regression tests asserting the version and sample audio
endpoints 404 when a stored path resolves to an existing directory
(guards the is_file() checks against regressing to exists()).
Addresses CodeRabbit review on PR #893.
Co-Authored-By: Claude Fable 5 <[email protected]>
* style(tests): drop parentheses on pytest.fixture decorator (ruff PT001)
Co-Authored-By: Claude Fable 5 <[email protected]>
* style(tests): satisfy Ruff naming rule
---------
Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: Jamie Pine <[email protected]>
* fix(setup): install mlx-lm and mlx-audio in setup-python on Apple Silicon
The dev setup installed requirements-mlx.txt but not mlx-audio/mlx-lm
themselves, so POST /transcribe failed on a fresh Apple Silicon setup
with "No module named 'mlx_audio'" (then "No module named 'mlx_lm'").
The release workflow already installs both with --no-deps (they declare
transformers>=5.x, conflicting with our <=4.57.x cap); mirror that in
the setup-python recipe with the same pins.
Co-Authored-By: Claude Fable 5 <[email protected]>
* test: add MLX smoke test for the --no-deps mlx-audio/mlx-lm install
mlx-audio and mlx-lm are installed --no-deps, so a missing transitive
dependency only surfaces at import time. Add a pytest-discoverable
smoke test (skipped off Apple Silicon) covering the exact entry points
the backend uses: mlx_audio.tts.load, mlx_audio.stt.load (which also
exercises the miniaudio dep from issue #505), mlx_lm.load/generate,
and a basic mlx.core op.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
Docker compose sets HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION:-}
which results in an empty string when not provided. An empty string is
not the same as unset - ROCm treats it as 'force-empty' and no GPU is
detected, even natively supported ones (e.g. gfx1201 / RX 9070 on ROCm 7.2).
Pop the env var when it is empty, before torch loads, so ROCm auto-detects
the GPU correctly.
Tested on RX 9070 (gfx1201) with ROCm 7.2 and PyTorch 2.12.1+rocm7.2.
The Windows `build-server` just recipe only built and copied the
voicebox-server sidecar, omitting the voicebox-mcp stdio shim that the
Unix scripts/build-server.sh builds via `build_binary.py --shim`.
As a result `just build` on Windows produced only one sidecar and the
Tauri bundle step failed with:
resource path `binaries\voicebox-mcp-<triple>.exe` doesn't exist
Build and copy the shim sidecar after the server, mirroring
build-server.sh. Hoist the triple/binaries-dir setup ahead of both
builds so the shim step reuses them.
Co-authored-by: namu.shin <[email protected]>
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
list_stories() previously executed one COUNT(story_items) query per
story in a Python loop. With N stories that is N+1 round-trips to
SQLite regardless of list length. Replace with a single aggregated
GROUP BY query that fetches all counts at once, then populate each
StoryResponse from a dict lookup.
force_offline_if_cached flips HF_HUB_OFFLINE (env + huggingface_hub
constant + transformers._is_offline_mode) process-wide for the duration
of a cached LLM load, silently switching every concurrent model
download/load on other threads to offline mode. With default capture
settings (whisper-turbo STT + Qwen3 refinement + auto_refine) a first
run downloads several models concurrently, and a poisoned fetch
surfaces as "Can't load feature extractor..." (whisper) or
"Unrecognized model ... model_type" (Qwen3) rather than anything
mentioning offline mode.
These are the last two call sites of the guard — the same pattern was
deliberately removed app-wide in #524/#530 after identical failures,
and the 0.5.0 LLM backend reintroduced it. LLM loads now run with the
process's default HF_HUB_OFFLINE state, matching every other backend
(issue #462 precedent).
Fixes#841
Claude-Session: https://claude.ai/code/session_011iwL9AyeAWgz2jpgcHxJpC
Co-authored-by: Claude Fable 5 <[email protected]>
force_offline_if_cached flips HF_HUB_OFFLINE (env + huggingface_hub
constant + transformers._is_offline_mode) process-wide for the duration
of a cached LLM load, silently switching every concurrent model
download/load on other threads to offline mode. With default capture
settings (whisper-turbo STT + Qwen3 refinement + auto_refine) a first
run downloads several models concurrently, and a poisoned fetch
surfaces as "Can't load feature extractor..." (whisper) or
"Unrecognized model ... model_type" (Qwen3) rather than anything
mentioning offline mode.
These are the last two call sites of the guard — the same pattern was
deliberately removed app-wide in #524/#530 after identical failures,
and the 0.5.0 LLM backend reintroduced it. LLM loads now run with the
process's default HF_HUB_OFFLINE state, matching every other backend
(issue #462 precedent).
Fixes#841
Claude-Session: https://claude.ai/code/session_011iwL9AyeAWgz2jpgcHxJpC
Co-authored-by: Claude Fable 5 <[email protected]>
* feat(i18n): add Korean (ko) locale with 559 translation keys
* fix(i18n): complete Korean translations for current UI
---------
Co-authored-by: Jamie Pine <[email protected]>
* feat(i18n): add Korean (ko) locale with 559 translation keys
* fix(i18n): complete Korean translations for current UI
---------
Co-authored-by: Jamie Pine <[email protected]>
Adds Spanish as a UI display language, matching the existing
4-locale pattern (en, ja, zh-CN, zh-TW) with full key parity.
- app/src/i18n/locales/es/translation.json: 832 strings across 18
namespaces, translated from the en master. Keys, {{interpolation}}
placeholders, <code>/<path>/<link>/<strong> tags and _one/_other
plurals preserved. Brand/model names (Whisper, Qwen3, CUDA, MCP…)
left untranslated by design.
- app/src/i18n/index.ts: register `es` in SUPPORTED_LANGUAGES and
resources; the language switcher and LanguageCode derive automatically.
- app/src/lib/utils/format.ts: wire the date-fns `es` locale for
relative-date formatting.
Verified: key parity 832/832 (no missing/extra, placeholders & tags
intact), biome check clean, app+web typecheck pass, build:web succeeds.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Co-authored-by: Jamie Pine <[email protected]>
Adds Spanish as a UI display language, matching the existing
4-locale pattern (en, ja, zh-CN, zh-TW) with full key parity.
- app/src/i18n/locales/es/translation.json: 832 strings across 18
namespaces, translated from the en master. Keys, {{interpolation}}
placeholders, <code>/<path>/<link>/<strong> tags and _one/_other
plurals preserved. Brand/model names (Whisper, Qwen3, CUDA, MCP…)
left untranslated by design.
- app/src/i18n/index.ts: register `es` in SUPPORTED_LANGUAGES and
resources; the language switcher and LanguageCode derive automatically.
- app/src/lib/utils/format.ts: wire the date-fns `es` locale for
relative-date formatting.
Verified: key parity 832/832 (no missing/extra, placeholders & tags
intact), biome check clean, app+web typecheck pass, build:web succeeds.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Co-authored-by: Jamie Pine <[email protected]>
The dictate pill window is built hidden at setup and the frontend emits
dictate:hide as soon as it mounts. The handler calls
set_ignore_cursor_events(true) on a window GTK has never realized, and
tao's CursorIgnoreEvents path unwraps the missing GdkWindow
(tao-0.34.5 event_loop.rs:449), panicking inside a glib dispatch that
cannot unwind — the process aborts within seconds of launch on Linux.
The click-through toggle exists as a macOS workaround for transparent
always-on-top NSWindows lingering as invisible click targets; it was
never needed on Linux. Gate all three call sites so Linux never toggles
it: the true/false pair stays balanced (never set, never unset), and
macOS/Windows builds are unchanged.
Co-authored-by: Claude Fable 5 <[email protected]>
TaskManager.error_download() intentionally keeps a failed task in the
active list (status="error") so /tasks/active can surface the error
and retry UI — but /models/status derived its "downloading" flag from
the same unfiltered list. One failed download therefore showed the
model as downloading:true / downloaded:false for the life of the
process, masking the model's real cache state (even a fully valid
on-disk cache) until an app restart. Likely behind endless-spinner
reports like #181 and the restart-fixes-it pattern in #883.
Add TaskManager.get_pending_downloads() (downloading/extracting only)
and use it in /models/status; /tasks/active behavior is unchanged.
Fixes#925
Claude-Session: https://claude.ai/code/session_011iwL9AyeAWgz2jpgcHxJpC
Co-authored-by: Claude Fable 5 <[email protected]>
/transcribe wrote every upload to a temp file named .wav regardless of its
real format. librosa picks its decoder from the extension, so any non-wav
upload failed with "could not open/decode file" even though the format is
one the app handles elsewhere.
profiles.py already solves this for voice samples by keeping the uploaded
extension when it is one of the audio types it accepts, and falling back to
.wav otherwise. Same approach here, same set. The fallback means an unknown
or missing extension behaves exactly as it does today.
The dictate pill window is built hidden at setup and the frontend emits
dictate:hide as soon as it mounts. The handler calls
set_ignore_cursor_events(true) on a window GTK has never realized, and
tao's CursorIgnoreEvents path unwraps the missing GdkWindow
(tao-0.34.5 event_loop.rs:449), panicking inside a glib dispatch that
cannot unwind — the process aborts within seconds of launch on Linux.
The click-through toggle exists as a macOS workaround for transparent
always-on-top NSWindows lingering as invisible click targets; it was
never needed on Linux. Gate all three call sites so Linux never toggles
it: the true/false pair stays balanced (never set, never unset), and
macOS/Windows builds are unchanged.
Co-authored-by: Claude Fable 5 <[email protected]>
TaskManager.error_download() intentionally keeps a failed task in the
active list (status="error") so /tasks/active can surface the error
and retry UI — but /models/status derived its "downloading" flag from
the same unfiltered list. One failed download therefore showed the
model as downloading:true / downloaded:false for the life of the
process, masking the model's real cache state (even a fully valid
on-disk cache) until an app restart. Likely behind endless-spinner
reports like #181 and the restart-fixes-it pattern in #883.
Add TaskManager.get_pending_downloads() (downloading/extracting only)
and use it in /models/status; /tasks/active behavior is unchanged.
Fixes#925
Claude-Session: https://claude.ai/code/session_011iwL9AyeAWgz2jpgcHxJpC
Co-authored-by: Claude Fable 5 <[email protected]>
/transcribe wrote every upload to a temp file named .wav regardless of its
real format. librosa picks its decoder from the extension, so any non-wav
upload failed with "could not open/decode file" even though the format is
one the app handles elsewhere.
profiles.py already solves this for voice samples by keeping the uploaded
extension when it is one of the audio types it accepts, and falling back to
.wav otherwise. Same approach here, same set. The fallback means an unknown
or missing extension behaves exactly as it does today.
Carry focus and auto-paste permission per capture, support dictation over macOS fullscreen Spaces, preserve native window behavior flags, and abort paste when the pill cannot be hidden safely.\n\nVerified: frontend CI; cargo check; diff and security scans. Packaged multi-Space validation remains a release gate.
Route MLX load, inference, unload, reset, cache cleanup, and shutdown through a single worker. Add affinity and concurrent-unload regression coverage.\n\nVerified: 17 related backend tests; frontend CI; cargo check.
* Add "Log in with browser" cloud device login
Connects the desktop app to Voicebox Cloud without the user ever handling an
API key. One button in Settings → General opens the system browser to
voicebox.sh, the user authorizes while signed in, and the credential lands
back in the app automatically.
Backend (FastAPI):
- /cloud/login/start opens the browser to the cloud authorize page with a
state we mint; the existing loopback server catches the redirect at
/cloud/callback and exchanges the one-time code (server-to-server, over TLS)
for a voicebox_ API key, verifies it against the API, and stores it.
- /cloud/status and /cloud/disconnect back the settings UI.
- state round-trip guards against login-CSRF; the key never crosses a browser
URL and is never exposed to the frontend (status returns a prefix only).
- CloudSettings singleton row; config gains VOICEBOX_CLOUD_URL /
VOICEBOX_CLOUD_API_URL (default the prod hosts, overridable for dev).
Frontend (React):
- CloudSection in Settings → General: "Log in with browser", polls status,
shows the connected device + a dashboard link. API keys are the advanced
path only, surfaced in the web dashboard.
The key is stored in the local app DB for now; OS keychain is a marked
follow-up.
* Address review feedback on cloud login
- time out status polling after 2 min so an abandoned browser flow
doesn't leave the button stuck on "Waiting for browser…"
- handle non-JSON / non-object payloads from the exchange and account
endpoints instead of 500ing after the state is consumed
- make singleton row creation race-safe (IntegrityError -> re-query)
- clear device_name on disconnect along with the rest of the metadata
- serve the dashboard URL from /cloud/status so the Manage link follows
VOICEBOX_CLOUD_URL instead of hardcoding production
- keep a "Disconnecting…" label on the disconnect button while pending
* Remove orphaned react-qr-code entries from lockfile
bun.lock was out of date with package.json (react-qr-code was removed
without reinstalling), failing the frozen-lockfile install in CI.
* feat(windows): add native ROCm support for AMD GPUs
Implements native ROCm architecture for Windows.
- Adds backend build pipeline for voicebox-server-rocm.exe
- Detects AMD GPUs dynamically and routes PyTorch allocations
- Adds automatic download and update logic for ROCm dependencies
- Refactors UI in GpuPage.tsx and GpuAcceleration.tsx to add AMD flows
- Fixes 'Switch to CPU' lock on Windows via Tauri backend_override state
- Resolves PyInstaller/rocm_sdk UnboundLocalError silent crashes
- Resolves Numba/NumPy 2.x incompatibilities during Qwen3-TTS load
- Resolves HF_HUB_OFFLINE Catch-22 for CustomVoice processor caching
* fix(rocm): host libs archive under the app release tag, drop offline-load regression
Align the ROCm libs download with the CUDA pattern: both the server core and
the libs archive are published under the app-version release tag, with the libs
content version encoded in the filename only. The previous code fetched libs
from a separate rocm7.2-v1 tag, which disagreed with the download test.
Also revert the unrelated Qwen CustomVoice changes that wrapped model loading in
force_offline_if_cached (not imported — a NameError on load for every platform)
and re-added a Base-model cache gate. The inference-path offline guard was
deliberately removed previously.
* feat(rocm): gate download on AMD detection and persist the backend variant
The ROCm download section now only shows when the backend reports an AMD GPU on
Windows (new supports_rocm health field, backed by the memoized
is_amd_gpu_windows detection that was previously unused), or when ROCm is already
downloaded/active.
Make the backend override honor a pinned variant: set_backend_override persists
the choice to disk so it survives an app restart, start_server reads it back,
and a cuda/rocm pin now actually selects that variant instead of always
preferring ROCm. A stale pin to a deleted backend self-heals to the default
order rather than forcing CPU. Add the web no-op stub for the new method.
* chore(rocm): drop incomplete vitest harness for the unused GpuAcceleration component
GpuAcceleration.tsx is not routed anywhere (GpuPage is the live settings view),
and the added vitest setup referenced testing-library/vitest deps that were not
in the lockfile, breaking the web typecheck. Remove the dead component's test
and its scaffolding to keep this PR scoped to the ROCm feature.
* ci(rocm): add ROCm release-artifact pipeline
Mirror the CUDA packaging path for ROCm so the runtime download has artifacts to
fetch. scripts/package_rocm.py splits the PyInstaller --rocm onedir into
voicebox-server-rocm.tar.gz (core) + rocm-libs-rocm7.2-v1.tar.gz (AMD runtime:
HIP DLLs, rocBLAS Tensile data, MIOpen kernel DBs) + rocm-libs.json, matching
the names services/rocm.py expects, both under the app-version release tag.
The new build-rocm-windows job in release.yml builds on windows-latest/cp312 and
lets build_binary.py --rocm pull the official AMD Radeon wheels.
The file classifier can't be validated against a real AMD build on CI, so it has
unit coverage (test_package_rocm.py) against a synthetic onedir layout. The
prefixes/dir markers may need a tweak after the first real build on AMD
hardware — the packager hard-fails loudly if it classifies zero ROCm files.
---------
Co-authored-by: Jamie Pine <[email protected]>
* Fix ROCm setup for Linux AMD GPUs
- Ensure Docker ROCm builds resolve PyTorch packages from the ROCm wheel index so later dependency installs do not replace them with CUDA wheels.
- Move ROCm device group handling to a runtime entrypoint that joins the groups owning /dev/kfd and /dev/dri, avoiding distro-specific render/video GID defaults.
- Leave HSA_OVERRIDE_GFX_VERSION unset by default in the ROCm compose overlay so newer RDNA GPUs can use native ROCm detection.
- Add Linux GPU detection to the Unix setup recipe so AMD systems install ROCm torch wheels and NVIDIA systems install CUDA wheels before backend dependencies.
* docs(changelog): add Linux ROCm setup entry
* fix(setup): pin ROCm torch wheels and prefer NVIDIA over amdgpu
- Install torch/torchaudio from the ROCm index only, before the pooled
requirements install, so a plain PyPI (CUDA) wheel can't outrank +rocm
- Detect NVIDIA before AMD and gate ROCm on /dev/kfd, so hybrid
AMD+NVIDIA hosts get CUDA instead of ROCm
* fix(docker): add ROCm GPU support via compose overlay
Fixes#618. The Docker image installs CPU-only PyTorch from PyPI by
default, so even when users correctly pass /dev/kfd and /dev/dri device
nodes into the container, torch.cuda.is_available() returns False and
the GPU is reported as "None (CPU only)".
Changes:
- Dockerfile: add PYTORCH_VARIANT build arg (default: cpu). When set to
"rocm", the ROCm-enabled PyTorch wheels are installed from the
pytorch.org/whl/rocm6.3 index before requirements.txt runs, so pip
sees the ROCm build as already satisfying the torch>=2.2.0 constraint
and does not overwrite it with the CPU wheel. The render and video
groups are created with parameterised GIDs (RENDER_GID / VIDEO_GID,
defaulting to Ubuntu 22.04 values) and the voicebox user is added to
both groups so it can open /dev/kfd and /dev/dri.
- docker-compose.rocm.yml: new compose overlay that wires everything
together — PYTORCH_VARIANT=rocm build arg, /dev/kfd + /dev/dri device
passthrough, group_add for render/video, HSA_OVERRIDE_GFX_VERSION
(defaults to 11.0.0 for RDNA3/Strix Halo with a comment listing
values for RDNA2/RDNA1/Vega), and PYTORCH_HIP_ALLOC_CONF for the
memory allocator. Usage:
docker compose -f docker-compose.yml -f docker-compose.rocm.yml up --build
- docker-compose.yml: add a comment pointing to the ROCm overlay.
The CPU default path is unchanged — no extra build time, no size increase.
Co-authored-by: Cursor <[email protected]>
* fix(docker): address review comments on ROCm overlay
Two issues raised in PR review:
1. CodeRabbit: `docker compose up --build-arg` is not supported by the
`up` subcommand. Replaced the GID override instructions with the
correct env-var export pattern. Added RENDER_GID and VIDEO_GID to
`build.args` using ${VAR:-default} interpolation so a single export
covers both the Dockerfile group creation and the runtime group_add.
Changed group_add entries from hardcoded strings to the same
interpolated vars so host GIDs stay in sync end-to-end.
2. @Xarianne: ROCm 6.3 does not support RDNA 4 (RX 9000 series) cards.
Added a ROCM_VERSION build arg (default 6.3) to both the Dockerfile
and docker-compose.rocm.yml so users can set ROCM_VERSION=7.2 for
RDNA 4 support without editing any files. Added RDNA 4 / 12.0.0 to
the HSA_OVERRIDE_GFX_VERSION comment table.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
Previously, HSA_OVERRIDE_GFX_VERSION=10.3.0 was unconditionally set for
all AMD GPUs, which caused suboptimal performance on RDNA 3/4 GPUs
(gfx11xx/gfx12xx) that have native ROCm support.
Now uses rocminfo to detect all GPUs and only sets the override for
systems where the oldest GPU needs it (RDNA 2 and older, gfx10xx and
below). Newer GPUs are left untouched.
Addresses CodeRabbit review:
- Case-insensitive regex matching on lowercased line
- Log level changed to INFO for rocminfo failures
- Multi-GPU support: iterates all GPUs, uses oldest for decision
Fixes#469
Signed-off-by: Amitesh Gupta
Signed-off-by: Amitesh Gupta
Signed-off-by: singlaamitesh <[email protected]>
* Add French (fr) language support
- Create app/src/i18n/locales/fr/translation.json with full UI translations
- Register French in SUPPORTED_LANGUAGES and i18next resources
- Wire French locale from date-fns for relative date formatting
* Fix Vite file watcher EBUSY error on Windows by excluding Rust target directory
Adds a complete pt-BR translation (832 strings, full parity with en)
and registers it in the i18n config and language selector.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
- /token: dedicated page with on-chain transparency (liquidity lock +
buyback/burns), holder utility, official-token clarity, and FAQ. The
landing now shows a teaser linking to it; navbar + footer repointed
from pump.fun to /token.
- /blog: file-based markdown blog (gray-matter + marked) with per-post
Open Graph images; first post "Why Voicebox has a token".
- /cloud: end-to-end encrypted backup & sync product page.
- /pricing: Local / Cloud / Studio tiers with a monthly/annual toggle;
$VOICEBOX holders get Cloud free.
- Add Testimonials section to the landing.
- Navbar: remove API/Download, add Pricing/Blog, fix center-nav spacing.
- docker-compose: host port 17493->17600 for local dev coexistence.
- Remove the sponsor feature (sponsors page, homepage promo, footer
link, Stripe constants, and the app About-page sponsored-by section)
- Add $VOICEBOX token: navbar pill linking to pump.fun and a footer
Token column with a copyable Solana contract address
- Drop API and Download from the navbar links