mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 12:50:42 -07:00
f4d7c86ec0d5fbcd951b1b241ae36d168e71e9ea
632
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f4d7c86ec0 |
fix(ui): parse naive-UTC timestamps consistently in formatAbsoluteDate (#953)
* 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]> |
||
|
|
abe16de6ee |
fix(mcp): add model_size parameter to voicebox.speak (#895)
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
|
||
|
|
af2308ea38 |
fix(rocm): add MIOpen stability env vars to docker-compose.rocm.yml (#865)
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. |
||
|
|
0c730104e2 |
fix(docker): normalize CRLF line endings on Windows checkouts (#951)
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 |
||
|
|
9116d381cc |
fix(linux-audio): select monitor device by name instead of setting PULSE_SOURCE (#949)
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 |
||
|
|
e70e639838 |
fix(transcription): transcode uploads to WAV before STT (#957)
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]> |
||
|
|
eab3d45192 |
Fix runaway MLX Qwen audio chunks (#964)
* fix runaway MLX Qwen audio chunks * test: tighten runaway retry coverage --------- Co-authored-by: huanghua01 <[email protected]> |
||
|
|
0d31687638 |
fix(tada): run voice-prompt encode under torch.inference_mode (#955)
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]> |
||
|
|
f7b45cc89e |
fix(macos): set Command flag on Cmd-down event so Electron apps paste (#952)
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. |
||
|
|
65b1e3cc6e |
chore: remaining biome lint fixes
Safe fixes from the same pass as the biome baseline commit (import type, arrow function in an inline script) that were missed when staging by file. |
||
|
|
cb5a800445 |
test(app): add bun test infra and first unit tests
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. |
||
|
|
000c13b6b9 |
docs: add code quality audit
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. |
||
|
|
827ce2bf0a |
docs: refresh contributor docs and drop root requirements.txt
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. |
||
|
|
f2e55ba50f |
fix(dictation): run hotkey commands off the main thread
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. |
||
|
|
b7b7d62b7d |
perf(db): enable WAL and busy timeout for SQLite
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. |
||
|
|
c5f9d3b0f3 |
fix(backend): close model load races
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__. |
||
|
|
11934c2b7d |
fix(mlx): fail generation when voice cloning fails
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. |
||
|
|
9fddf3f799 |
build: pin git dependencies to commits
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. |
||
|
|
7c7421177d |
chore(app): delete unused generated API client
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.
|
||
|
|
f82cf67acd |
ci: gate lint, backend tests, and cargo check on PRs
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. |
||
|
|
02fa924b42 |
chore: move biome config to biome.jsonc and baseline existing violations
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. |
||
|
|
b434db22f6 |
chore(backend): repair test suite and bring ruff to green
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. |
||
|
|
766c51a8a1 |
fix(backend): numpy compat hook silently failed to patch torch
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. |
||
|
|
3b0c29249b |
fix: repair .gitignore encoding and cover local workspace dirs
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. |
||
|
|
88e72d5da2 |
fix(dictation): preserve native focus and fullscreen injection
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. |
||
|
|
5787e36611 |
fix(dictation): make microphone lifecycle race-safe
Coalesce and invalidate microphone acquisition, preserve immediate stop/cancel events, block overlapping/finalising takes, and add the opt-in keep_mic_warm setting with privacy-preserving default off.\n\nVerified: frontend CI; keep_mic_warm migration default and idempotency; diff checks. |
||
|
|
5fd95b3dcc |
fix(mlx): serialize accelerator lifecycle and inference
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. |
||
|
|
f2cf2a729d |
Add "Log in with browser" cloud device login (#812)
* 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. |
||
|
|
b542768429 | Update PROJECT_STATUS.md | ||
|
|
e766c7cbfb |
feat(windows): Native AMD ROCm GPU Acceleration (Resolves #531) (#538)
* 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]> |
||
|
|
c2282b256a |
fix: ROCm setup for Linux AMD GPUs (#817)
* 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 |
||
|
|
cabef1bfe0 |
fix(docker): add ROCm GPU support via compose overlay (#630)
* 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]> |
||
|
|
3835b63bd8 |
fix(backend): detect AMD GPU before setting HSA_OVERRIDE_GFX_VERSION (#785)
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]> |
||
|
|
da79e37ef5 | remove redundent section | ||
|
|
6e4989313c | remove clorb | ||
|
|
42b9cae216 | Add transparency stats to landing page | ||
|
|
b9bb2f075c |
feat: french translation (#802)
* 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 |
||
|
|
e294b9c8f0 |
feat(i18n): add Brazilian Portuguese (pt-BR) locale (#810)
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]> |
||
|
|
c1814a2870 | Create CLOUD_ROADMAP.md | ||
|
|
7d9a384ee4 |
Add $VOICEBOX token page, blog, and cloud/pricing pages
- /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. |
||
|
|
c6a59f4477 | claude moment | ||
|
|
4f13123b95 | update project status & responsible use | ||
|
|
21c7e373d3 | Add $VOICEBOX token section below the hero | ||
|
|
45b64e0233 |
Replace sponsor program with $VOICEBOX token
- 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 |
||
|
|
b35b90961d | Add Trendshift badge to README | ||
|
|
7df366d0c8 |
feat: 0.5.0 Capture release — dictation, MCP, personalities (#544)
* feat(capture): dictation, personalities, 0.5.0 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]> * feat(mcp): local MCP server exposes voicebox.* tools to AI agents Mounts FastMCP at /mcp (Streamable HTTP) so Claude Code, Cursor, Windsurf, and the VS Code MCP extensions can call voicebox.speak, voicebox.transcribe, voicebox.list_captures, and voicebox.list_profiles against the running Voicebox server. Backend - new backend/mcp_server package (tools, middleware, profile resolve, pub/sub events); named mcp_server to avoid shadowing the installed mcp PyPI package FastMCP imports internally - app.py migrated from @app.on_event to lifespan= so FastMCP's session manager cohabits with Voicebox's startup/shutdown - new MCPClientBinding table + /mcp/bindings CRUD; ClientIdMiddleware reads X-Voicebox-Client-Id into a ContextVar and stamps last_seen_at - profile resolution precedence: explicit -> per-client binding -> capture_settings.default_playback_voice_id - POST /speak REST wrapper for non-MCP callers (shell, ACP, A2A) - GET /events/speak SSE broadcasts speak-start / speak-end so the pill surfaces agent-initiated speech - backend/mcp_shim proxy (plain httpx) for stdio-only MCP clients - PyInstaller spec updates + new --shim build target (~18 MB) Frontend - Settings -> MCP page with HTTP / stdio / claude-mcp-add copy snippets, default voice picker, per-client bindings table, connection status - useMCPBindings, useSpeakEvents hooks - CapturePill gains 'speaking' state; DictateWindow subscribes to SSE and emits dictate:show so the Rust side surfaces the pill window Native - tauri.conf.json externalBin now includes voicebox-mcp - show_dictate_window helper + dictate:show listener in main.rs - (also in this commit: InputMonitoringGate UX, hotkey_monitor tweaks, landing footer/navbar updates, new overview docs for captures / dictation / mcp-server / voice-personalities) Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat(mcp): Rust-owned speaking pill with self-contained audio playback 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]> * readme and dev script * feat(capture): gate global hotkey on dictation readiness checklist 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]> * color * model download status * progress * personality: bool API, i18n across the app - 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 }); ``` * i18n: GenerationPage sidebar copy * fix: BOOL import for windows crate 0.62 BOOL moved from Win32::Foundation to windows::core in 0.62. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(capture): layout-aware V keycode for synthetic paste on macOS macOS apps match Cmd+V against the layout-translated character via NSMenu key equivalents, so posting kVK_ANSI_V (= 9, the QWERTY V position) on Dvorak produces Cmd+. and never triggers Paste. New keyboard_layout module resolves the active layout's V keycode via TISCopyCurrentKeyboardLayoutInputSource + UCKeyTranslate, caches it in an AtomicU16, and refreshes on kTISNotifySelectedKeyboardInputSourceChanged. All TIS calls run on the main thread (init from Tauri setup; observer callback delivered to the main runloop); synthetic_keys::send_paste reads the cached value once per paste. Falls back to kVK_ANSI_V when resolution fails or the active input source carries no Unicode key layout data. Windows is intentionally left on hardcoded VK_V — SendInput delivers WM_KEYDOWN with wParam = VK_V to the target regardless of the active layout, which is why `Send "^v"` works for AutoHotkey on Dvorak Windows. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(capture): cooperative app activation for synthetic paste on macOS 14+ macOS 14 deprecated NSRunningApplication.activateWithOptions: in favour of a cooperative-activation pattern: the caller first yields activation rights to the target, then the target activate()s against the tightened Sonoma foreground rules. Without the yield, activate() on 14+ sometimes silently fails or only bounces the dock icon — the exact "paste lands in the wrong app" symptom we were previously one API break away from. activate_pid now discovers the 14+ selector via respondsToSelector: and branches: on 14+ it yieldActivationToApplication:'s from NSRunningApplication.current then calls -activate on the target; on 11–13 it stays on -activateWithOptions: (still the only option). Both branches propagate the BOOL return — if activation is refused we error out before clobbering the clipboard instead of silently proceeding. The respondsToSelector: result is cached in a OnceLock so the probe isn't repeated on every paste. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(capture): conditional clipboard restore + always-attempt on paste failure Two bugs in paste_final_text' clipboard handling: 1. Restore was unconditional. If the user ⌘C'd in the target app during the 400 ms paste-consume window — or a clipboard history tool (Paste, Pastebot, Maccy) or Universal Clipboard sync snapshotted our staged text — the blind restore overwrote their newer content with the pre-paste snapshot, silently losing user data. 2. send_paste' errors were propagated with ? before the restore, so a CGEventPost / SendInput failure left the user's clipboard stuck on the transcript. Fix folds both into one pattern: capture the post-write change count, re-read it after paste-consume, restore only when they match (plus treat a change-count read failure as "unknown, don't overwrite"). Isolate send_paste's error so the restore runs regardless of paste success, then propagate the paste error after. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * chore(deps): pin rdev to jamiepine/rdev fork Upstream Narsil/rdev has shipped no release since 2023-06 (crates.io still serves 0.5.3), so the Sonoma main-thread fix we depend on — PR #147, applied at hotkey_monitor.rs:184 — is only reachable via a git pin. A pin to a third-party repo breaks the build whenever the remote force-pushes, renames, or is taken down, and Cargo does not durably cache git-dep archives the way it does crates.io tarballs. Forking to jamiepine/rdev at the same SHA removes that failure mode without changing crate behavior and gives us a place to cherry-pick future OS-compatibility fixes on our own timeline. The SHA was verified to exist on the fork before re-pinning. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(mcp): idle timeout + escalating backoff for speak-SSE monitor Two reliability gaps in the /events/speak subscriber: 1. resp.chunk().await had no idle timeout. A backend that accepts the TCP connection but stops producing frames (deadlocked SSE endpoint, zombie process) would block the task forever without reconnecting. The pill window would never surface for agent-initiated speech and there would be nothing to log. Backend emits a `:ping` heartbeat every 15 s, so 45 s without any data is now treated as a dead stream — the task errors out and the reconnect loop takes over. 2. Flat 2 s backoff escalates nowhere. Logs fill with reconnect lines when the backend is down for minutes, and a backend that accepts + immediately closes connections (no data) spins the loop tightly. Backoff now escalates 500 ms → 30 s on unproductive rounds and resets only when at least one frame arrives (the connection was genuinely productive, not just accepted). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(refinement): character-level loop collapse + pytest coverage The word-level pass catches single-word Whisper loops ("URL URL URL…") but misses two common hallucination patterns the PR had to claim as "edge cases": 1. Multi-word English loops — "thanks for watching thanks for watching…" × 6 sails through because no two consecutive tokens are identical after text.split(). 2. CJK loops — "謝謝觀看" × 7 sails through because text.split() returns a single unsplit token for the whole loop (no whitespace between characters). Add a character-level second pass: a non-greedy regex finds any 2–60 char substring that repeats min_run+ times immediately after itself and strips the run. The 2-char floor keeps emphasised single-letter runs ("wooooooow") intact. The 60-char ceiling covers every observed Whisper tail hallucination ("Please like and subscribe to my channel.", "Subtitles by the Amara.org community") while staying short enough that coincidental long-phrase repetition in legitimate speech doesn't hit the threshold. Whitespace normalisation only runs when the pass actually stripped something, so untouched transcripts keep their original spacing. New test_refinement_collapse.py gives the pre-processor its first deterministic unit-test coverage: 17 tests pinning the word-level legacy behaviour plus the new multi-word English / CJK / Japanese / emphasis-preservation cases. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(db): graceful fallback when SQLite < 3.35 on MCP bindings migration SQLite gained ALTER TABLE … DROP COLUMN in 3.35 (Mar 2021). Production PyInstaller builds bundle Python 3.12 which links to SQLite 3.40+ so that path is always safe, but a dev running the backend directly on Ubuntu 20.04 (3.31) or Debian 11 (3.34) would crash on first startup trying to drop the legacy default_intent column. Add _supports_drop_column(engine) — returns True on non-SQLite dialects (Postgres / MySQL have supported DROP COLUMN for decades) and gates on the runtime sqlite_version for SQLite. When unsupported, log a warning and leave the unused column in place: SQLAlchemy only maps declared columns, so a stray default_intent column does no reads or writes and can't interfere with runtime behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(mcp): correct lifespan shutdown order — drain MCP before unloading models The inline lifespan ran _run_shutdown inside the MCP context, so the TTS / Whisper / LLM models were unloaded *before* FastMCP's __aexit__ got a chance to cancel its in-flight session tasks. Any MCP request mid-generate at shutdown time would crash on "model unloaded" instead of receiving a clean session-cancelled error. Rewire via compose_lifespan (which was already defined in mcp_server.server for exactly this purpose but never used): AsyncExitStack enters factories in order and exits in LIFO, so MCP teardown fires first — cancelling sessions — and _run_shutdown runs after nothing is holding the models. Smoke test shows the log order flipped as expected: Ready StreamableHTTP session manager started ... running ... StreamableHTTP session manager shutting down ← was last, now first Voicebox server shutting down... ← was first, now last As a side benefit, _run_shutdown is now paired with _run_startup via try/finally inside voicebox_lifespan, so a partial startup (models half-loaded, MCP __aenter__ fails) still unloads whatever was loaded instead of leaking it to process exit. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(mcp): stamp last_seen_at on /speak too + tighten path predicate POST /speak is a REST wrapper around voicebox.speak for agents that don't talk MCP (shell scripts, ACP, A2A). It reads X-Voicebox-Client-Id and uses it for the same per-client profile resolution + default personality lookup the MCP tool does (speak.py:39-64), so its callers are first-class clients — but the ClientIdMiddleware only stamped last_seen_at on /mcp* paths. REST speak callers showed up as "never seen" in Settings → MCP despite actively acting on their bindings. Widen the stamp predicate to an explicit ("/mcp", "/speak") prefix list, and require a path boundary on match so future routes named /mcpfoo or /speakers don't silently inherit the stamp via the prefix. New test_client_id_middleware.py pins the scope with 17 parametrised cases (both the allowed set and the overlap cases that must not match). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat(captures): scrubbable WaveSurfer player for capture detail view Replace the placeholder fake-waveform + play button in CapturesTab's audio card with a real CaptureInlinePlayer (wavesurfer.js). The player renders the actual waveform, lets users scrub through the clip, and shows a proper current/total timestamp pair in place of the duration-only label. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat(ui): persist selectedProfileId across sessions Wrap useUIStore in zustand/middleware's persist under the key voicebox-ui. partialize only selectedProfileId so volatile UI state (dialog open flags, form drafts, engine/voice pickers, sidebar) stays in-memory as before — but reopening the app no longer loses whichever profile the user was last working with. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat(captures): mirror readiness checklist into the settings sidebar The six-gate checklist only rendered in the CapturesTab empty state, so a user already on the settings page had no single surface showing which gate was red — the inline InputMonitoringNotice covered one, the model pickers covered another, and Accessibility was only hinted at by the auto-paste toggle. Mirror the same component into the right sidebar of the settings page so every gate (STT model, LLM model, Input Monitoring, Accessibility, plus the hotkey toggle in the main column) is always visible while the user configures dictation. New compact prop on DictationReadinessChecklist drops the centered header and empty-state max-width so it fits the 280 px sidebar next to the existing About / Differences blocks. Callers in compact mode own the heading — CapturesPage reuses the existing captures.readiness.title key (present in en / ja / zh-CN / zh-TW already) as an h3 matching the sibling sidebar sections. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat(captures): move sidebar checklist below differences + hide when all green Two small follow-ups to the sidebar checklist placement. Move it below the What's different section so the sticky top of the sidebar stays the page's narrative context (About → differences) and the checklist reads as a status panel rather than preamble. Gate the whole block on !readiness.allReady so once every gate is green the sidebar drops back to just About + What's different — no value in real estate full of checkmarks. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(captures): refetch readiness immediately after STT/LLM model swap useCaptureSettings updated its own cache optimistically but never invalidated ['capture-readiness'], so for up to 5 s (the poll interval) after switching stt_model or llm_model the checklist kept showing the previous model's ready/missing state. The backend endpoint resolves the model live on each call — it was just the frontend cache that lagged. Invalidate in onSettled only when the patch touched a model field, so unrelated updates (chord keys, toggles) don't pay for a refetch. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(captures): hide macOS-only copy when running on Windows / Linux Two surfaces leaked macOS-specific copy onto other platforms: 1. The Input Monitoring + Accessibility rows in the readiness checklist rendered everywhere. On Windows/Linux the Rust permission stubs return true, so the rows showed as permanent green checkmarks with copy like "macOS allows Voicebox to detect your global shortcut." — nonsense when you're on Windows. Gate both rows on a userAgent-based isMacOS check so they only render where the underlying TCC permission actually exists. 2. The global-shortcut setting description ended with "macOS will ask for Input Monitoring permission the first time you turn this on." That sentence rendered on every platform. The readiness checklist already surfaces the TCC requirement at the right moment on macOS, so the description doesn't need the platform note — drop it from en / ja / zh-CN / zh-TW. Other macOS strings (AccessibilityNotice, InputMonitoringNotice, their "stillMissing" hints) are already gated behind the Rust permission booleans returning false, which never happens on Windows/Linux, so they stay inert without further changes. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat(capture): swap the rdev fork for keytap 0.2, delete local chord state machine Dep swap: - Drop the git-pinned jamiepine/rdev fork we were carrying since the upstream crate is abandoned. - Depend on keytap 0.2 from crates.io — our own cross-platform global keyboard tap crate. Clean shutdown via Drop, Sonoma-safe by design (no TSMGetInputSourceProperty calls off the main thread, so `set_is_main_thread(false)` is gone), and properly versioned. Chord engine rewrite: - Delete hotkey_monitor.rs's internal Chord state machine (Match enum, KeyEvent enum, step()/classify() methods, associated unit tests). keytap's ChordMatcher subsumes it: Momentary chord for PTT, add_toggle() for Toggle-to-talk, longest-match resolution, sticky-end for Toggle. Net: -80 LOC in hotkey_monitor.rs; the remaining module is the dispatcher loop + Effect→Tauri translation. - Preserve the PTT→Toggle "RestartRecording" upgrade signal. keytap emits End(PTT)+Start(Toggle) atomically (same Instant) when the held set upgrades from a shorter chord to a longer superset. The dispatcher peeks at the matcher with a 5 ms recv_timeout after any End and coalesces the pair into Effect::RestartRecording so the frontend still gets the "discard the transition-moment audio" signal instead of an unrelated Stop+Start pair. - HotkeyMonitor::update_bindings now actually tears down the tap on empty bindings instead of leaving an idle CGEventTap around. New bindings rebuild the matcher and the dispatcher thread from scratch. key_codes.rs: - Rewrite the browser-code → Key table against keytap's cleaner Key variant names (`A`..`Z` not `KeyA`..`KeyZ`, `Digit0`..`Digit9` not `Num0`..`Num9`, `ArrowUp` not `UpArrow`, `AltLeft`/`AltRight` instead of `Alt`/`AltGr`, `Period` not `Dot`, …). On-disk chord string format (W3C `KeyboardEvent.code` identifiers) is unchanged, so capture_settings rows written before the swap round-trip identically. Legacy aliases (`Alt`, `AltGr`, `Num0`, `UpArrow`, `Dot`, …) kept for forward-compat on old rows. main.rs / input_monitoring.rs: - Update the few doc comments that referenced `rdev::listen` to describe keytap's Tap; no behavioural change. - build_chord_bindings now imports from keytap::Key. - enable_hotkey / disable_hotkey / update_chord_bindings reach into HotkeyMonitor via &mut since apply()/update_bindings() now mutate. Tests live in keytap now (22 chord-related tests in keytap 0.2, including the PTT→Toggle upgrade scenario that used to be tested in hotkey_monitor.rs). Voicebox's hotkey_monitor.rs is thin enough that local testing would be trivia. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * chore(deps): bump keytap 0.2 → 0.4 for macOS modifier-events fix 0.2 read CGEventFlags via CGEventGetIntegerValueField(event, 0x81), which is not a valid CGEventField id — macOS silently returned 0, so FlagsChanged events produced no KeyDown / KeyUp for any modifier key and the PTT / toggle chords never armed on macOS. 0.4 uses the documented CGEventGetFlags(event) API. 0.3 (tracing / serde / Fn / IntlBackslash) is picked up as a free consequence; no API surface we depend on changed. * perf(captures): stop polling readiness once both models are green useQuery was firing GET /capture/readiness every 5s forever, and also on every window focus. Once stt.ready and llm.ready are both true the answer can only change when the user swaps a model in settings, and useSettings already invalidates the query on that path — the polling was pure noise. Gate both refetchInterval and refetchOnWindowFocus on "not fully ready" so we fall silent once the checklist is green. * feat(ui): theme settings, stories polish, track editor restructure - add dark/light/system theme with persisted choice + OS change listener - restyle stories sidebar (search, item layout, border) to match captures - move floating generate box to right column of stories, add top fade mask - story track editor: sticky track labels aligned via flex rows, custom scrollbar with left/right zoom handles - capture pill light mode pass, fix inline waveform progress color - pull mcp_server hidden imports into the pyinstaller spec - notarization doc draft * fix mlx llm bundling * feat(ui): shared ListPane primitive + misc polish 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 * fix(captures): Play As autoplay + default voice + orphan recovery - 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. * fix(captures+chord): Stop button stops, ChordPicker accepts shorter chords Two unrelated correctness bugs caught in PR review: - The Play As "Stop" button was wired to handlePlayAs() unconditionally, so clicking it during playback kicked a fresh generation instead of halting. Now pauses the player when the click came from the main button while playbackState is 'playing'. Picking a different voice from the dropdown still kicks a new generation as before. - ChordPicker tracked the peak set of held keys but seeded the peak from initialKeys, so a user who opened the picker with a 3-key chord saved couldn't replace it with a 2-key chord — the candidate length never beat the seed. The peak now resets on the first press of a fresh sequence (when no keys were held immediately prior), then grows monotonically within that hold. * fix(settings): honor explicit null on nullable fields, ignore on the rest 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. * fix(captures): clean up audio files when create_capture fails The create flow wrote raw audio (and a transcoded .wav for non-wav sources) to data/captures before the DB row was committed, so any failure between the write and the commit — a webm that decoded to a 0-length array, a whisper model that errored mid-transcribe, a SQLite contention on the commit — left the audio on disk with nothing pointing at it. Over enough flaky uploads the directory grows without bound. Now every path written before the commit is tracked in a list, and the whole stretch from the first write to db.commit() runs inside a try/except that unlinks each tracked file on raise and re-raises. The transcode branch removes the raw file from the cleanup list only when the unlink actually succeeds, so an OSError on the raw-path delete still hands cleanup the original blob to retry. * fix(mcp): restrict voicebox.transcribe(audio_path=...) to loopback audio_path mode took any absolute filesystem path and returned its decoded contents as transcribed text with no caller verification beyond the existence/size checks. The X-Voicebox-Client-Id middleware records the header but never rejects an absent or fake one, so a Voicebox bound to 0.0.0.0 (the documented "remote access" mode) was effectively an unauthenticated arbitrary-local-file read primitive. The middleware now stashes the request's remote address in a ContextVar alongside the existing client_id, and audio_path mode refuses anything that doesn't parse as a loopback address (IPv4 127.0.0.0/8, IPv6 ::1). audio_base64 mode is unchanged — that path was always bounded to bytes the caller already has. Loopback callers (the Tauri webview, local CLI scripts, MCP clients on the same machine) keep working. Remote callers now have to send the audio over the wire if they want it transcribed. * fix: PR review nits — response shape, landing copy, form reset - /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. * perf(mcp): move last_seen_at stamp off the request path ClientIdMiddleware was running the SQLAlchemy SELECT/INSERT/UPDATE/COMMIT inline on the event loop after every /mcp/* and /speak request. SQLite serialises writes, so concurrent MCP traffic queued behind the stamp write — the response sat waiting on a side-effect that the client never needs in band, and SSE streams would stall briefly per request. The middleware now hands the stamp to asyncio.to_thread via a fire-and- forget create_task so the response returns immediately and the write runs on the default executor. A module-level set keeps strong refs to in-flight tasks (per asyncio docs) so the GC can't collect them mid- write. The fallback path runs the stamp inline if no loop is available (tests/oddball callers) rather than silently dropping it. * fix(dictate): force-dismiss the speaking pill when SSE never comes back The pill subscribed to /generation/{id}/status to know when to start playback, but EventSource.onerror was a no-op — auto-reconnect was the intended recovery for transient drops. The gap: if the backend deletes the gen row mid-flight or the connection silently dies in a way the browser keeps retrying without ever getting a status event, the pill sits in 'speaking' forever and the user has no way to clear it. Added a 60-second hard cap that arms when the SSE opens and clears the moment any real status event lands. If it fires while the pill is still on the same id and audio never started, it force-dismisses. Same idea as the existing post-speak-end 15s grace, but covers the case where the backend never says anything at all. * fix: i18n cleanup + readiness checklist effect cadence + ChordPicker shadow - DictationReadinessChecklist was constructing downloadByModel as a fresh Map every render and listing it in the cleanup effect's deps. With the 1 s polling cadence and arbitrary parent rerenders the effect ran more often than it needed to. Memoised the Map on activeTasks; the effect now keys off the memo's identity. - zh-CN persona tooltipActive/ariaLabelActive matched their inactive twins byte-for-byte ("以人物设定朗读"). The other locales differentiate the active state with a -ing / -中 suffix; zh-CN now reads "正以人物设定朗读" when active. - personalityPlaceholder was a ~290-character paragraph that doubled as both the example text and the explanation, repeating most of what personalityHint already said. Trimmed to the example only and folded the explanation + leave-blank consequence into the hint, across all four locales. - Refinement model size keys were size06 / size17 / size4. Renamed the 4B variant to size40 so the decimal padding is consistent. - ChordPicker's open-effect bound a window.setTimeout id to a local `t`, shadowing the i18n `t` from useTranslation. Renamed to timeoutId. * perf(settings): persist generation sliders on release, not per pointer-move Both sliders on the generation settings page were calling update() — which is a React Query mutation that PATCHes /settings/generation — inside onValueChange. Dragging the chunk-limit slider from 800 to 3000 fired a request per pointer-move pixel, and a mid-drag failure plus optimistic rollback would leave persisted state visibly out of sync with the thumb position. Local state now mirrors each slider during a drag and the persist happens once on Radix's onValueCommit (pointer-up / keyboard-release). useEffects keep the local state in sync if the persisted value changes out-of-band — another window editing the same setting still updates the slider position cleanly. * chore(backend): Ruff lint pass — deprecated APIs, exception leaks, dead patterns Mechanical sweep of items called out in the PR review: - qwen_llm_backend: AutoModelForCausalLM.from_pretrained(torch_dtype=…) is deprecated in transformers ≥4.41 in favor of dtype=. Renamed. - routes/llm: try/except around backend.generate() raised HTTPException(500, detail=str(e)) which leaks stack traces / paths to clients and trips Ruff B904. Now logs the original exception server-side and hands the client a generic message; chained via `from e` to preserve traceback context. - mcp_bindings + mcp_server/context: datetime.utcnow() is deprecated since 3.12. Switched the two assignment sites to datetime.now(timezone.utc). The schema-level `default=datetime.utcnow` defaults in database/models.py are left for a later schema-aware pass. - routes/generations: `logger = …` sat between two import blocks (Ruff E402). Moved below imports. - mcp_server/server + tests/test_refinement_samples: typing.Callable / typing.Iterable have been preferred-via collections.abc since 3.9 (Ruff UP035). - routes/events: `except asyncio.TimeoutError` aliases plain `TimeoutError` since 3.11 (UP041). - services/captures: hoisted WHISPER_NATIVE_FORMATS to module scope (was a function-local UPPER_SNAKE that tripped N806) and replaced the raw_path.unlink try/except OSError-pass with contextlib.suppress (SIM105). Semantic equivalence preserved — written_files.remove(raw_path) still only runs when unlink succeeds because it sits inside the suppressed block after the unlink call. - database/migrations: hoisted the duplicate `import sqlite3` from inside two helper bodies to a single module-level import. * feat(stories): regenerate action on clips and the chat list dropdown The track editor's clip toolbar now has a regenerate icon next to Delete; clicking it kicks a fresh take of the selected clip's underlying generation through the same /generate/{id}/regenerate path the History table uses, and pushes the id into the global pending set so the SSE watcher picks it up. The chat list's per-item dropdown gets the same action between Play-from-here and Remove. Translation keys added under storyContent.itemActions / storyContent.toast across all four locales. * feat(stories): import external audio into the timeline (drag-drop + picker) You can now drop a music file onto the story content area or pick one through the new "Import audio" button in the add-clip popover. Both call POST /generate/import which writes the file to data/generations/<id>.<ext>, probes duration via librosa, and inserts a Generation row pointing at a singleton "Imported Audio" profile (created lazily on first import). The existing addStoryItem flow takes over from there — the timeline doesn't care that the row didn't come out of TTS. Engine field on the row is "import"; it's surfaced on StoryItemDetail so the chat list shows a music icon instead of the (missing) profile avatar and both the dropdown and the track-editor toolbar hide the Regenerate action — there's nothing to regenerate. Accepted formats: wav/mp3/flac/ogg/m4a/aac/webm, capped at 200 MB. Translation keys added across en/ja/zh-CN/zh-TW. * fix(audio): serve real Content-Type so imports decode in WaveSurfer /audio/{id} and /audio/version/{id} hardcoded media_type="audio/wav" on the FileResponse. That was a no-op when every generation came out of TTS (everything on disk was a .wav anyway), but imported audio keeps its source format — .mp3 / .m4a / .ogg — and the WaveSurfer MediaElement backend uses an <audio> tag that checks Content-Type before letting the clip play, so an MP3 announced as audio/wav silently failed to load. Both endpoints now derive the type via mimetypes.guess_type and fall back to audio/wav for unknown suffixes. Download filenames also keep the real extension instead of always saying ".wav". * feat(stories): zoom bar bounds tracked to project length, default 60s scope The track editor's zoom was clamped to a hardcoded [10, 200] pixels-per-second range, which had no relationship to the project — on a 4-minute story a "max zoom out" of 200 px/s still required scrolling, and on a 5-second story you could zoom all the way in to where every clip was a tiny sliver. Reframed the bounds in the unit the user actually thinks in: how many seconds of timeline are visible at once. Min scope is 10 s (most zoomed in), max scope is the entire project, and the default lands on a 60 s scope (or the full project, whichever is shorter) once the editor measures its visible track width on first mount. The pixels-per-second value still lives in component state (because every downstream calculation already uses it) but minPps/maxPps are computed from `containerWidth − LABEL_COL_WIDTH` and the project's effective duration, so the +/- buttons and the edge-drag handles on the scrollbar all clamp to bounds that move with the project. Re-clamping fires whenever those bounds shift — adding a long clip or resizing the window pulls the current zoom inside the new range instead of leaving the user parked outside it. * fix(stories): show the source filename on imported clips Imports were rendering as "Imported Audio" everywhere because every import points at the singleton voice profile. The filename was already being stored on the generation row (in the `text` field), so the chat item title and the timeline clip label now read from `text` when `engine === 'import'` and fall back to the profile name otherwise. The chat item also drops the language pill (always "en" on imports — not informative) and skips the transcript textarea since imports have no spoken text to show. * fix(stories): round split_time_ms before posting handleSplit was sending currentTimeMs - item.start_time_ms straight to the backend, which rejects it because StoryItemSplit.split_time_ms is typed as int and the playhead's currentTimeMs is a float (it's driven from HTMLAudioElement.currentTime, which carries sub-millisecond precision). Pydantic surfaced the mismatch as "Input should be a valid integer, got a number with a fractional part" and the toast read "Failed to split clip". Math.round at the call site, matching what the trim and move handlers already do. * feat(stories): per-clip volume control on the timeline Each story item now carries a volume column (linear gain, default 1.0, clamped 0.0–2.0 server-side). New PUT /stories/{}/items/{}/volume route + useUpdateStoryItemVolume hook + a Volume2 icon in the clip-edit toolbar that opens a popover with a 0–200% slider. Local slider state drives the visual during a drag; the persist fires once on onValueCommit, mirroring the generation-page slider pattern. Web Audio playback inserts a per-clip GainNode between source and master so volume changes apply live without re-decoding the buffer (source -> clipGain -> masterGain -> destination). Server-side mixdown in export multiplies the trimmed clip by its volume before summing into the timeline. Split + duplicate carry the volume forward to the new clips so trimming a faded section keeps the level you set. Migration adds the volume column with default 1.0 so existing rows read as full volume. * fix(stories): mute the clip waveform's media element so it can't bleed audio The clip waveforms drawn inside each timeline track use WaveSurfer with the default MediaElement backend, which creates an internal <audio> element to drive playback timing. Web Audio in useStoryPlayback is what actually produces sound, but WaveSurfer's element was happily preloading and — after the first user gesture unlocked browser autoplay — playing the source URL through the page output too. For TTS clips it was masked: they're short, both sources start at the same time, and stopping the BufferSourceNode at pause coincides with the natural end of the audio element. For long imports (a four-minute MP3) the BufferSourceNode stops on pause but WaveSurfer's element keeps going on its own track — which is exactly the "music keeps playing when I pause" symptom. Hand WaveSurfer a muted <audio> element via the `media` option so the visual still loads peaks but the element itself can never produce sound. preload="metadata" keeps the load lightweight. * fix(stories): hard-cut the audio graph on stop so long imports actually halt source.stop() was the only thing happening when a clip was halted, and on long imported buffers (multi-minute MP3s scheduled via source.start with a duration argument) it was silently failing to halt the buffer in some browsers — pause left the music playing and seek stacked another source on top of the original. The mute-the-WaveSurfer-element fix was a different bug along the same path; this is the one that actually addresses the duplicated audio. ActiveSource now carries the per-clip GainNode alongside the source, and stopSource detaches the onended handler before calling stop() (so the natural-end callback can't race with explicit teardown and re-delete a freshly rescheduled entry at the same id), then disconnects both nodes inside their own try/catch blocks. Even when stop() doesn't actually halt the buffer the graph is severed — no path from source to destination, no audio. * feat(stories): add empty tracks above/below the timeline Tiny + strips sit at the top of the topmost label cell and the bottom of the bottommost one, sticky-positioned in the label column so they follow horizontal scroll. Clicking either extends the visible track stack in that direction by one — above adds max(existing)+1, below adds min(existing)-1. Both compute against the full set (defaults + item-derived + previously-added) so successive clicks keep extending instead of fighting over the same number. Empty extras live in component state because a track only earns its keep once a clip lands on it. Once one does, item.track carries the number forward and the row keeps deriving from items naturally; if nothing lands there before reload, the empty row simply isn't there next time, which matches what the user expects of an unused affordance. * fix(mcp): bundle stdio shim sidecar * fix(captures): allow dictation without paste permission * fix(mcp): preserve speak engine defaults * fix(captures): use platform hotkey defaults * fix(mcp): preload speak pill window * fix(captures): hide unwired storage settings * feat(sponsors): add /sponsors page, homepage promo, and in-app strip * style(landing): drop pill chrome from /download maintainer kicker * changelog * better naming for sponsors * windows keybind note --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> |
||
|
|
627d40b42d |
Fix web API URL for remote access (#550)
Default production web builds to the page origin so Docker and LAN users do not fetch from browser-local localhost. Preserve the local/Tauri fallback and repair stale persisted loopback URLs. |
||
|
|
ed2eec591a | Bump version: 0.4.4 → 0.4.5 v0.4.5 | ||
|
|
d61e884104 |
fix(offline): patch transformers mistral-regex check to survive HF failures (#530)
* fix(offline): patch transformers mistral-regex check to survive HF failures transformers 4.57.x's `PreTrainedTokenizerBase._patch_mistral_regex` calls `huggingface_hub.model_info(repo_id)` unconditionally during any non-local tokenizer load to probe for Mistral-family models. The call raises on `HF_HUB_OFFLINE=1`, on network outages, and on slow/blocked HF endpoints, and transformers doesn't catch any of it — the exception bubbles out of `from_pretrained` and kills the load for unrelated engines (Qwen TTS, Qwen CustomVoice, TADA, etc.). 0.4.2's load-time `force_offline_if_cached` guard walked straight into this trap: on cached online users it flipped `HF_HUB_OFFLINE=1` and converted a healthy load into a hard crash. 0.4.3's inference-path guard masked it; #524 removed the inference guard in 0.4.4, and users updating to 0.4.4 started hitting the same error on the load path instead (#526). Fix: - Wrap `_patch_mistral_regex` so any exception from the inner HF metadata check is swallowed and the tokenizer is returned unchanged. Voicebox never loads Mistral models, so the regex rewrite this check gates is a no-op for us; matches the success-path behavior for non-Mistral repos (tokenization_utils_base.py:2503). - Drop the `force_offline_if_cached` wraps from every load path (pytorch_backend Qwen + Whisper, qwen_custom_voice_backend, mlx_backend Qwen + Whisper). With the mistral patch in place they provide zero value and only risk re-introducing the same class of bug. Helper and its unit tests stay — still correct for targeted future use. - Add `backend/tests/test_offline_patch.py` covering OfflineModeIsEnabled / ConnectionError suppression, success pass-through, idempotence, and the missing-method no-op path. Fixes #526. * fix(offline): install mistral-regex patch for non-MLX backends The previous commit left the patch wired only through ``mlx_backend.py``'s existing import of ``hf_offline_patch``. On Windows/Linux/CUDA users who never load the MLX backend (everyone who hit #526), the patch module was never imported, so ``patch_transformers_mistral_regex`` never ran and the crash persisted. Hoist the import into ``backends/__init__.py``. Every backend imports from this package, so the module-level patch install runs before any ``from_pretrained`` call regardless of which engine the user picks. Caught by CodeRabbit and Cursor Bugbot on #530. |
||
|
|
74e004400f | Bump version: 0.4.3 → 0.4.4 v0.4.4 |