The floating generate box is fixed at the bottom of the viewport, so
all of its Select dropdowns (voice profile, language, engine, effects)
opened downward into — or beyond — the window edge. Add side="top" to
each SelectContent so the menus appear above their trigger instead.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
key_from_str() had no arm for "Function", so it fell through to
None. Since build_chord propagates that as a hard Err via ?, binding
any chord containing fn made build_chord_bindings fail entirely —
HotkeyMonitor was never spawned, silently killing both push-to-talk
and toggle-to-talk until the chord was reverted.
Every other layer (keytap's macOS key tap, Key::Function itself, the
frontend's canonicalKeyFromEvent/displayLabelForKey) already handles
fn — only this string-to-Key bridge was missing the arm.
Fixes#941
Export filenames were derived from only the first 30 characters of the
generation text. Generations with similar wording (a common workflow when
iterating on the same line) produced identical filenames, so exports
collided on disk — the browser appended " (1)"/" (2)" and users ended up
opening audio that didn't match the expected filename.
Append the first 8 chars of the generation id to the .wav and .voicebox.zip
export filenames, in both the backend Content-Disposition headers and the
frontend save-file hooks.
Co-authored-by: Claude Opus 4.8 <[email protected]>
`UploadFile.filename` can be None, and `Path(None)` raises TypeError. On the
avatar endpoint this happens before the try/except, so a filename-less upload
surfaces as an unhandled 500 instead of a clean response. Every other upload
handler already guards this with `file.filename or ""` (add_profile_sample,
transcription, generations); apply the same guard here.
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
* fix(ui): parse naive-UTC timestamps consistently in formatAbsoluteDate
Backend timestamps are naive UTC (Python `datetime.utcnow()`) and are
serialized without a timezone suffix. `formatDate` already normalizes
these by appending `Z` before parsing, but `formatAbsoluteDate` called
`new Date(date)` directly. Per the ES spec, a timezone-less date-time
string is parsed as local time, so absolute timestamps were shown off by
the viewer's UTC offset (e.g. +9h in JST) — and disagreed with the
relative time rendered by `formatDate` for the same value (visible in the
Captures detail panel, which uses both on `capture.created_at`).
Extract the normalization into a shared `parseServerDate` helper and use
it in both formatters.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* docs(format): clarify parseServerDate comment on date-only vs date-time parsing
ECMAScript parses date-only strings ("2026-07-23") as UTC but timezone-less
date-time strings ("2026-07-23T10:00:00") as local time. The backend emits the
latter, which is the case this helper normalizes. Corrects the comment per PR
review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* docs(format): trim parseServerDate comment to match surrounding style
Reduce the multi-line explanation to a single why-comment consistent with
other utils comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
The MCP voicebox.speak tool built its GenerationRequest without a
model_size, so every agent-triggered generation fell back to the schema
default ("1.7B"). There was no way to reach the 0.6B Qwen variant (or
TADA's 1B/3B) through MCP, and callers paid a model reload whenever the
requested size differed from what was already loaded.
Thread an optional model_size through voicebox.speak and the _speak
helper into GenerationRequest, mirroring the REST /generate surface.
Omitting it passes None, which generate_speech normalizes to the engine
default, so existing callers are unaffected.
Add backend/tests/test_mcp_speak.py covering the forwarded value, the
omitted-default path, and rejection of an invalid size.
Fixes#884
Add three environment variables to prevent miopenStatusUnknownError and
system stuttering during inference on RDNA4 GPUs:
- MIOPEN_USER_DB_PATH: redirect MIOpen kernel cache to writable, persistent dir
- MIOPEN_CUSTOM_CACHE_DIR: same, for custom operator cache
- MIOPEN_FIND_MODE=FAST: use heuristic kernel selection instead of exhaustive
benchmarking, which fails on RDNA4 with ptr: 0 size: 0 workspace warnings
MIOPEN_FIND_MODE=FAST does not affect output quality. All MIOpen kernel
variants produce the same numerical result; fast mode selects a known-good
kernel using heuristics instead of benchmarking every variant on the GPU.
Tested on RX 9070 (gfx1201) with ROCm 7.2 and PyTorch 2.12.1+rocm7.2.
Hardware note: tested on Ryzen 7 9800X3D + RX 9070 with Gigabyte B650M DS3H
motherboard. The exhaustive benchmarking failures may be related to IOMMU
behavior on this platform. This system was affected by an IOMMU bug patched
upstream in kernel 6.19.10, which may be a contributing factor. May not
affect all RDNA4 systems. MIOPEN_FIND_MODE=FAST is a safe default regardless.
Depends on PR #862 which fixes the broken ROCm Docker build.
A Windows Git checkout with checkout-time CRLF conversion enabled
produces CRLF working-tree copies of package.json and
scripts/rocm-entrypoint.sh, breaking the Docker build two ways:
- The frontend stage's `sed -i -z 's/,\n ]/…/'` is LF-anchored, so
it doesn't match against \r\n and leaves an invalid trailing comma
in package.json, which then fails JSON parsing in the vite build.
- The final stage copies rocm-entrypoint.sh straight from the build
context; with a CRLF shebang the container reports the misleading
"no such file or directory" for an entrypoint that plainly exists,
because Linux can't resolve "/bin/sh\r" as an interpreter.
Add .gitattributes forcing LF for both files at checkout time, plus a
sed normalization step in each Dockerfile stage for resilience with
clones that predate the .gitattributes rule.
Fixes#915
std::env::set_var is not thread-safe on Unix (unsafe as of Rust 2024
edition) and calling it from a spawned capture thread while other
threads (tokio runtime, webview, Tauri plugins) may read the
environment is a data race risk. It also never got unset, so the
monitor source would leak into any later cpal/ALSA init in the same
process.
Replace the env-var indirection with direct device selection: when
pactl reports a monitor source name, search cpal's input device
enumeration for an exact match. Fall back to a substring match on
'monitor' (the original pactl-unavailable path), then the host's
default input device. This is the 'pass the source name directly to
cpal' option from the issue - no env mutation, no leakage between
capture sessions, and it still re-detects the current default sink's
monitor on every start_capture call.
Fixes#471
The /transcribe endpoint passed the raw uploaded file straight to the STT
backend (mlx_audio.stt -> miniaudio), which only decodes WAV/FLAC/MP3/Vorbis.
Browser recordings arrive as WebM/Opus (Chrome/Firefox MediaRecorder), so
web-mode dictation failed with 500 "unsupported file format". The Tauri app
was unaffected because WebKit produces MP4.
librosa already fully decodes the upload to compute duration (falling back to
audioread/ffmpeg for exotic containers), so re-encode that PCM to a temp WAV
and hand it to Whisper. WAV inputs pass through unchanged; the temp file is
cleaned up in the finally block.
Co-authored-by: Claude Opus 4.8 <[email protected]>
Encoder.eval() alone still builds an autograd graph because parameters
require grad by default. On 8GB GPUs that ballooned TADA encode VRAM far
past the model footprint (issue 890). Wrap the encode forward in
inference_mode and add a unit test that asserts the flag is set.
Co-authored-by: fooSynaptic <[email protected]>
The macOS auto-paste sequence in `send_paste` posted the Cmd-down
CGEvent with flags = 0, setting the Command flag only on the V events.
On real hardware the Cmd keyDown (a flagsChanged event) already carries
kCGEventFlagMaskCommand, and Chromium/Electron builds its tracked
modifier state from that flag. With flags = 0 the tracker stays at
"Command up", so the following V matches neither the Cmd+V accelerator
(tracker says no modifier) nor plain-text insertion (the V event's own
flags say Command is held) — Electron drops it silently, producing no
paste and no stray "v". AppKit reads the V event's own modifier flags
and pastes regardless, which is why native apps (Notes, TextEdit,
Warp) worked while Electron targets (Slack, VS Code, VS Code Insiders)
silently no-op'd.
Setting kCGEventFlagMaskCommand on the Cmd-down event makes the
flagsChanged event well-formed; Chromium then registers Command=down
and Cmd+V matches. Likely fixes#762 and #643.
bun test as the runner — already the toolchain, zero new deps beyond
@types/bun. 54 tests across 5 files covering the already-pure logic:
FastAPI error normalization (extracted verbatim to lib/api/errors.ts),
clip trim clamping (extracted from StoryTrackEditor to lib/utils/trim.ts,
magic 100 now MIN_CLIP_DURATION_MS), duration/size/engine formatters,
engine-language map consistency, and changelog parsing. Wired into CI
after typecheck.
Known issues surfaced by the tests, left as-is for now: parseChangelog's
heading regex uses \s* which matches newlines, so a dateless heading
followed directly by a bullet swallows that bullet; formatFileSize has
no TB unit; ENGINE_DISPLAY_NAMES lacks tada/kokoro.
Point-in-time review of the backend, frontend, Rust shell, and
CI/hygiene, with per-area grades, file:line evidence, a priority
list, and a follow-up ledger of what was fixed the same day.
CONTRIBUTING.md caught up with reality: ruff not Black, Python 3.12,
sh.voicebox.app, the routes/services/backends layout, the actual
pytest + CI story, and a working autoupdater link. SECURITY.md now
says 0.5.x and describes what CI actually enforces. The root
requirements.txt was vestigial (unpinned, included torchvision,
referenced by nothing) — deleted, with two stale doc references
repointed at backend/requirements.txt. backend/pyproject.toml is now
covered by bumpversion; its version was stuck at 0.2.3 and is synced
to 0.5.0.
enable_hotkey/disable_hotkey/update_chord_bindings were sync commands,
which Tauri runs on the main thread. update_bindings joins the
dispatcher thread, and the dispatcher's chord-effect path blocks on
main-thread window calls (outer_size, current_monitor, set_position) —
toggling the hotkey while an effect was in flight could deadlock.
Async commands run on the runtime pool, so the join no longer blocks
the thread the dispatcher is waiting on.
The generation worker and request handlers race on the database —
enough that orphan-recovery code exists in two places to clean up
after 'database is locked' failures. WAL lets readers proceed during a
write, synchronous=NORMAL is the recommended pairing, and the 30s
sqlite3 timeout waits on a locked database instead of raising
immediately.
Kokoro and LuxTTS load_model had no lock, so two concurrent requests
could both observe an unloaded model and double-load; they now use the
same double-checked asyncio.Lock pattern as the Chatterbox backends.
get_stt_backend gets the threading.Lock treatment the TTS and LLM
factories already had. Includes the ruff-era typing cleanup for
backends/__init__.
A cloning failure was caught and retried without the voice prompt, so
the user got the model's default voice recorded as a successful
generation. The error now propagates and the worker records the
generation as failed with the real message. Two sibling silent paths
raise as well: a model whose generate() lacks ref_audio support, and a
generation that produces no audio chunks.
linacodec and Zipvoice (both from a third-party personal account) and
Qwen3-TTS installed from branch HEADs, so a force-push upstream could
silently change what a release ships. linacodec/Zipvoice are pinned to
the commits resolved in the working venv; Qwen3-TTS to current
upstream HEAD, verified to be the installed 0.1.1.
app/src/lib/api/{core,models,schemas,services,index.ts} was
openapi-typescript-codegen output imported by nothing — the live
client is the hand-written client.ts/types.ts pair — and its types
had drifted (no engine, personality, or effects_chain on
GenerationRequest). Removes the generator with it: generate-api.sh,
the generate:api script, and the just recipe. Docs that described the
codegen workflow now describe updating the hand-written client.
The previous gate was typecheck + web build only — no lint, no Python,
no Rust, none of the 24 backend test files. Adds:
- biome lint to the frontend job
- backend-quality on macos-14 (matches the primary user platform so
the MLX-path tests run): just setup-python, ruff check, pytest
- rust-quality: cargo check with stub sidecar binaries, since
tauri-build validates externalBin paths and real sidecars only exist
in the release pipeline
- concurrency group so superseded runs cancel
All gates verified green locally before being wired in.
biome.json is strict JSON; the jsonc extension allows the annotations
on the baseline entries. Rules that currently fail are downgraded to
warn with a note against issue #421 — restore each to error as its
occurrences are fixed. files.experimentalScannerIgnores keeps the
scanner out of .worktrees/ so a local worktree's own config can't
conflict with the root one.
Also fixes the handful of auto-fixable errors (unused imports in docs/
and landing/, @ts-expect-error over @ts-ignore). bun run lint is now
green: 0 errors, 96 warnings visible as debt.
The suite hadn't run green since the routes refactor:
- test_profile_duplicate_names.py imported the pre-refactor module
layout and broke collection; now imports backend.services.profiles
- tests/conftest.py puts the repo root and backend dir on sys.path so
files collect standalone instead of depending on run order
- test_cors.py tested a hand-copied mirror of the origin list that had
drifted from app.py (missing http://tauri.localhost); it now builds
the app via the real create_app() factory
- test_progress.py simulated a 1KB download, below the tracker's 1MB
reporting threshold; simulation raised to 5MB
- slow/timeout markers registered in pyproject
Ruff: ~900 violations auto-fixed (typing modernization, import
sorting, unused imports, whitespace). The remaining rules are baselined
in pyproject.toml with per-rule counts to burn down, plus per-file
carve-outs for deliberate env-before-import ordering. ruff check is
now clean; suite is 134 passed, 2 skipped.
dtype_map referenced _t, which is only bound as a default argument on
the inner function, so building the map raised NameError. The
surrounding except swallowed it and returned, meaning the from_numpy
fallback this hook exists for never applied in frozen builds.
The tail of the file had been appended as UTF-16LE with CRLF line
endings, so git couldn't parse those patterns (including
.claude/settings.local.json). Rewritten as UTF-8/LF throughout, and
.worktrees/, .hermes/, and mlx-test/ are now ignored for everyone
instead of via .git/info/exclude.
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