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]>
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]>
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]>
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]>
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]>
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]>
- Collapse intent tri-state (respond/rewrite/compose) to `personality: bool` on /generate, /speak, and voicebox.speak. Drop respond entirely; keep compose as a standalone button via /profiles/{id}/compose. Remove /rewrite, /respond, and /speak profile endpoints.
- FloatingGenerateBox: Wand2 persona toggle + Dices compose button appear when the selected profile has a personality. ProfileCard badges Wand2 alongside the effects Sparkles.
- MCP bindings: default_intent column → default_personality: bool. Migration drops the legacy column.
- i18n: en / ja / zh-CN / zh-TW translation files filled out and wired through the capture, server, and profile UI.
```ts
voicebox.speak({
text: "Deploy complete.",
profile: "Morgan",
personality: true, // rewrite through the profile's personality LLM
});
```
Stops the "stuck pill" failure where pressing the chord with missing
STT/LLM models triggers a recording that has nowhere to land. The
hotkey now stays disarmed until every gate (models downloaded, Input
Monitoring + Accessibility granted) is green; the empty-state checklist
in CapturesTab surfaces each unmet gate with a one-click action and
auto-arms the chord once everything turns green.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The pill window now surfaces for agent-initiated speech without main-window
involvement. Rust subscribes to /events/speak via a tokio task + reqwest
streaming body (speak_monitor.rs), shows the pill, and forwards events to
the dictate webview over Tauri's event bus. The pill plays audio via a
plain HTMLAudioElement and emits dictate:hide when playback ends. The
pill stays hidden through the ~1 s generation wait and only surfaces when
audio actually starts, with the counter armed at that moment.
Fixes a shared-dict mutation in mcp_server/events.publish() that caused
the second subscriber (Rust speak_monitor) to receive `event: message`
instead of named speak-start/speak-end frames. Also teaches the speak_monitor
parser to handle CRLF framing (sse-starlette default). Main-window
AudioPlayer now skips autoplay for source in {mcp, rest} to avoid
double-play when both windows are alive.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Ships the Capture release end to end. Global-hotkey dictation with
synthetic paste into the focused app on macOS and Windows, an on-screen
pill across recording / transcribing / refining, customizable push-to-
talk and toggle chords, and an accessibility-permission prompt scoped to
Settings → Captures with inline re-check feedback.
Voice profiles gain optional personalities that power compose / rewrite /
respond actions via a local Qwen3 LLM — shared with refinement, so there
is one local LLM in the app, not two.
Refinement hardened with deterministic Whisper-loop collapse before the
LLM sees the transcript, per-capture flag snapshots for re-runs, and a
ten-transcript evaluation harness across every bundled refinement size.
Version bump 0.4.5 → 0.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): add i18next foundation with English + zh-CN locales
Installs i18next + react-i18next + language detector and wires up a
language selector in the General settings page. Extracts strings from
the highest-visibility surfaces: all settings tabs, model management,
sidebar nav, main editor, and the floating generate box. Remaining
strings (profile forms, history, stories/effects/voices/audio tabs)
can land in follow-up PRs.
Closes#411.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(i18n): ensure language switch actually re-renders the tree
- `nonExplicitSupportedLngs: true` was normalizing `zh-CN` → `zh` in
some code paths; since we have explicit `zh-CN` resources, swap it
for `load: 'currentOnly'` which keeps the code as-is.
- `react: { useSuspense: false }` — react-i18next v17 defaults Suspense
on, which can silently suspend components mid-switch and look like
"nothing happens" to the user.
- Use `i18n.language` (the raw current code) instead of
`resolvedLanguage` in the selector so the dropdown always mirrors
what we just set.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize ProfileCard, HistoryTable, and relative dates
- ProfileCard: "No description", "designed" badge, aria-labels, and
delete dialog.
- HistoryTable: delete / clear-failed / import / effects dialogs.
- formatDate: switch date-fns `formatDistance` locale based on
`i18n.language` so "5 minutes ago" becomes "5 分钟前" under zh-CN.
HistoryTable now subscribes via useTranslation so the table
re-renders when language flips.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize Stories tab (list, content, dialogs, toasts)
Covers the title + "New Story" button, empty states, story row
metadata (item count, updated time), the create/edit/delete dialogs,
and all toast notifications. Also handles StoryContent: "Select a
story" placeholder, search popover, "Export Audio" button, and the
"Generating N audios" pending indicator.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize history item and story item dropdown menus
Covers the "..." action menu on both the History table (Play, Export
Audio, Export Package, Apply Effects, Regenerate, Delete) and on
individual story chat items (Play from here, Remove from Story).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize Effects tab (list, detail, dialogs, toasts)
Covers EffectsList (title, "New Preset", section headers, preset
cards) and EffectsDetail (header buttons for Save / Save as Custom /
Delete, name/description fields, preview section, Save as Custom
dialog, all toasts).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize EffectsChainEditor and built-in preset names
- Effect type labels (Chorus/Flanger, Reverb, Delay, Compressor, Gain,
High-Pass, Low-Pass, Pitch Shift) and every param label (LFO speed,
Modulation depth, Threshold, Ratio, etc.) go through
`effects.types.<type>.{label,params.<param>}` with the backend string
as defaultValue fallback.
- Chain-level controls: "Load preset…", "Add effect…", "Clear",
Power/Remove button titles.
- Built-in preset names + descriptions (Robotic, Radio, Echo Chamber,
Deep Voice) are translated client-side; user-created presets keep
their original names.
Backend keeps returning English — frontend intercepts and translates
via key lookup, defaulting to the backend string so unknown
effects/params don't break.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize Create/Edit Voice modal and audio sample panels
ProfileForm now routes its title, description, voice-source toggle
(Clone from audio / Built-in voice), field labels (Name, Description,
Language, Engine, Voice, Reference Text, Default Engine, Default
Effects), sample tabs (Upload / Record / System Audio), action
buttons, and every toast + Zod validation message through i18n.
Also covers the three AudioSample panels (Upload/Record/System) — the
choose-file / start-recording / start-capture call-to-actions, the
"N remaining" countdown, "Recording complete" / "Capture complete"
states, and the Play / Transcribe / Remove / Record Again buttons.
SampleList too — the "No samples yet" empty state, per-sample edit
mode, mini-player aria labels, Delete Sample dialog, and toasts.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize Audio Channels tab (list, dialogs, device picker)
Covers the "Audio Channels" title and "New Channel" button, the empty
state, per-channel section labels (Output Devices / Assigned Voices),
the Available Devices right pane with its three contextual hints, the
"No voices assigned" fallback, and both Create/Edit dialogs (titles,
descriptions, field labels, Select placeholders, and the "(default)"
badge).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize Voices tab (table header, search, inspector)
VoicesTab now translates the "Voices" title, search placeholder,
"New Voice" button, all six table column headers (Name, Language,
Generations, Samples, Effects, Channels), the avatar alt text, and
the per-row channel MultiSelect (placeholder + "(Default)" suffix).
VoiceInspector routes its form labels through the existing
`profileForm.fields.*` keys, has its own "Default Effects" hint and
avatar/save toasts, and reuses the ProfileForm Zod validation.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): localize ProfileList unsupported-model note and empty state
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): add Traditional Chinese (zh-TW) locale
Adds a zh-TW translation with Taiwan vocabulary conventions
(e.g. 預設 / 儲存 / 載入 / 匯入 / 匯出 / 設定 / 檔案 / 伺服器 / 裝置 / 網路).
Registers it alongside en and zh-CN; the language dropdown picks it up
automatically from SUPPORTED_LANGUAGES.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* feat(i18n): add Japanese (ja) locale
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(i18n): localize relative dates for ja and zh-TW
formatDate only mapped zh-CN, so history timestamps stayed in English for
ja and zh-TW users even after the rest of the UI translated. Extend the
switch to ja and zhTW from date-fns/locale.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
* fix(i18n): address PR review feedback
Bugs:
- GeneralPage: network access toast used the keep-server-running title key
(wrong semantic scope). Add networkAccess.updatedTitle and use it.
- GeneralPage: fallback "Unknown" version was stored as a translated string
in state, so it stayed stale across language switches. Store null, resolve
the label at render time.
- GeneralPage: memoize the zod resolver on t and retrigger validation when
the locale changes so existing error messages retranslate.
- GpuPage: adding t to the CUDA progress EventSource effect deps caused the
SSE connection to be torn down and reopened on every language change,
potentially dropping in-flight download events. Capture t in a ref.
- HistoryTable: Effects dialog still rendered English "Source" / "Select
source version" / "Cancel" / "Apply" / "Applying..." — localize them.
- Locales: zh-CN / zh-TW / ja devSuffix was missing the leading space before
"(开发版)"/"(開發版)"/"(開発版)", so dev builds rendered "v0.4.2(开发版)"
instead of "v0.4.2 (开发版)".
Nits:
- ModelManagement: rename .find((t) => ...) callback param to avoid
shadowing useTranslation().t.
- GenerationPage: rename chunkLimit.value interpolation key from count →
chars so i18next doesn't silently activate pluralization if a translator
later adds _one/_other forms.
- LanguageSelect: narrow onValueChange handler param to LanguageCode.
Key count now 559 across en/zh-CN/zh-TW/ja (added 4 effectsDialog keys
plus networkAccess.updatedTitle).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Keep a silent looping <audio> element mounted at the app root so macOS
never tears down the CoreAudio session. Without this, backgrounding the
app long enough leaves WaveSurfer's AudioContext in a state where play()
resolves and timeupdate fires, but no audio reaches the output — and not
even cmd+R (full JS reload) restores it, only a full app relaunch.
Uses a zero-PCM WAV blob at full volume rather than a muted element,
since WebKit can optimize muted media away and defeat the purpose.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
ModelManagement.tsx reads migrationResult.moved (added in #433) but
apiClient.migrateModels() was typed as returning only { source, destination }.
The backend actually returns { moved: int, errors: list[str], source, destination }
(backend/routes/models.py:140, 168). Widen the TS return type so the check
typechecks under the new CI gate from #418.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
When user attempts to change model storage location with no models
downloaded, the migration API returns moved=0 early. Previously the UI
would still call setCustomModelsDir() and restart the server, causing
unexpected behavior (hang/connection lost).
This change checks migrationResult.moved === 0 and shows a helpful
toast message instead of proceeding with the storage change.
Fixes: #426
Co-authored-by: fuleinist <[email protected]>
Catches format drift that accumulated across 13 authored files in
app/src and docs/. Auto-generated artifacts (tauri/src-tauri/gen,
docs/openapi.json, docs/cli.json, app/src/lib/api) were left alone
since the build regenerates them on each run — baking their formatted
state into git just causes churn next build.
No behavioral changes. Trailing commas, line wrapping, and indentation
only.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The in-app changelog viewer rendered each entry's version number at the
same size as body text (text-sm font-medium), so visually there was no
clear anchor for where one release's notes ended and the next began.
Bump the version heading to text-xl font-semibold tracking-tight and
widen the bottom margin so each release reads as a proper section
header.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Before 0.4 every engine was a cloning model, so the instruct UI in the
floating generate box applied the same way everywhere. Commit 3187344
hid the instruct toggle because the mix of new engines landing in 0.4
made it unclear which ones honored the kwarg. With Qwen CustomVoice
now shipping as the only engine actually tuned for instruct-style
control, bring the button back — conditionally, and only for that
engine.
Changes:
• FloatingGenerateBox: SlidersHorizontal toggle button appears left
of Generate when the box is expanded AND engine is
qwen_custom_voice. Clicking it reveals an additive instruct
textarea below the main text field (not a modal swap like the old
version). State persists across engine switches so the toggle
remembers its last position.
• GenerationForm: narrow the instruct FormField's conditional from
`qwen || qwen_custom_voice` to just `qwen_custom_voice`.
• useGenerationForm: narrow supportsInstruct for the same reason.
Base Qwen3-TTS accepts the kwarg but the model itself doesn't honor
it — only CustomVoice was trained for instruction-based style
control.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Add cleanup for requestAnimationFrame and setTimeout in scroll effect
to prevent stale DOM writes on unmount or rapid selection changes
- Fix disabled+selected card click: bounce the selection to re-trigger
the engine auto-switch instead of deselecting
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Show all voice profiles with unsupported ones grayed out (opacity) instead of hidden
- Clicking a grayed-out profile selects it and auto-switches the engine to a compatible one
- Sort supported profiles first, with info tip about compatibility at the bottom
- Scroll to selected profile after engine/sort changes with safe margin
- Fix engine desync on tab navigation by initializing form engine from store
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When the model wasn't loaded, the app was closed mid-run, or a
generation otherwise errored out, the resulting "Failed" rows
accumulate in history and there was no way to remove them in bulk —
individual delete was the only option.
Adds a header row above the history list (only rendered when at
least one failed generation is present) with a "Clear failed" button
that opens a confirmation dialog, then calls a new
DELETE /history/failed endpoint which sweeps all status='failed'
rows (plus their version files / audio files on disk).
Closesjamiepine/voicebox#410
Co-authored-by: Claude Opus 4.6 <[email protected]>
Two changes to address the race condition causing "Failed to split clip":
Backend (stories.py): Added with_for_update() to the item query in
split_story_item so concurrent requests for the same clip are
serialized via a row lock instead of racing.
Frontend (StoryTrackEditor.tsx): Guard handleSplit with
splitItem.isPending to prevent rapid double-clicks from firing
multiple mutations before the first completes.
Fixes#366
Co-authored-by: Matt Van Horn <[email protected]>
- Rust: Replace fragile body.contains("status") with proper JSON
deserialization validating status=="healthy", model_loaded (bool),
and gpu_available (bool) to prevent misidentifying non-Voicebox services
- Frontend: Validate health response has Voicebox-specific fields before
marking server as ready during fallback polling
- Frontend: Discriminate port-in-use errors (poll for external server) from
real startup failures (missing sidecar, signing issues) — surface errors
immediately with a startupError state and Retry button in the UI
- Frontend: Set explicit startup-error state when 2-minute polling timeout
expires so the loading screen shows actionable feedback instead of hanging
- Architecture: Extract QueryClient to standalone side-effect-free module
(lib/queryClient.ts) to decouple serverStore from React bootstrap entrypoint
Add Kokoro-82M as a new TTS engine — 82M params, CPU realtime, 8 languages,
Apache 2.0. Unlike cloning engines, Kokoro uses pre-built voice styles, which
required a new profile type system to support non-cloning engines cleanly.
Kokoro engine:
- New kokoro_backend.py implementing TTSBackend protocol
- 50 built-in voices across en/es/fr/hi/it/pt/ja/zh
- KPipeline API with language-aware G2P routing via misaki
- PyInstaller bundling for misaki, language_tags, espeakng_loader, en_core_web_sm
Voice profile type system:
- New voice_type column: 'cloned' | 'preset' | 'designed' (future)
- Preset profiles store engine + voice ID instead of audio samples
- default_engine field on profiles — auto-selects engine on profile pick
- Create Voice dialog: toggle between 'Clone from audio' and 'Built-in voice'
- Edit dialog shows preset voice info instead of sample list for preset profiles
- Engine selector locks to preset engine when preset profile is selected
- Profile grid filters by engine — shows Kokoro voices when Kokoro selected
- Custom empty state when no preset profiles exist for selected engine
Bug fixes:
- Fix relative audio paths in DB causing 404s in production builds
- config.set_data_dir() now resolves to absolute paths
- Startup migration converts existing relative paths to absolute
Also updates PROJECT_STATUS.md and tts-engines.mdx developer guide.
Two fixes for issue #312:
1. GUI stuck on loading screen when backend is already running externally
(e.g. via python/uvicorn/Docker):
- Rust: add HTTP health check fallback when the process on the port
doesn't have 'voicebox' in its name. If /health responds with a
valid Voicebox response, reuse the server instead of erroring.
- Frontend: when startServer() fails, fall back to polling the
health endpoint every 2s instead of permanently blocking.
2. No data refresh when switching server URLs in settings:
- serverStore.setServerUrl() now invalidates all React Query caches
when the URL actually changes, so profiles/history/models/stories
are re-fetched from the new server.
- Export queryClient from main.tsx for store-level cache invalidation.
Fixes#312
Upgrade CUDA toolkit from 12.6 (cu126) to 12.8 (cu128) for proper
RTX 50-series (Blackwell) GPU support. Users with RTX 5070/5080/5090
were reporting CUDA detection failures with cu126.
Also fix the GPU Acceleration settings panel where the 'Switch to CPU
Backend' button was unreachable — it was inside a conditional block
that required !isCurrentlyCuda, making it impossible to switch back
to CPU once running on CUDA.
Closes#315
Integrates HumeAI's TADA (Text-Acoustic Dual Alignment) speech-language
model as a new TTS engine. TADA uses a novel 1:1 token-audio alignment
that produces coherent speech over long sequences (700s+).
Two model variants:
- tada-1b: English-only, ~4GB, built on Llama 3.2 1B
- tada-3b-ml: 10 languages, ~8GB, built on Llama 3.2 3B
Backend uses the Encoder for voice prompt encoding with caching, and
TadaForCausalLM with flow-matching diffusion for generation. Supports
bf16 inference on CUDA, forces CPU on macOS (MPS compatibility).
Installed with --no-deps due to torch>=2.7 pin conflict; descript-audio-codec
and torchaudio added as explicit sub-dependencies.
Use refetchQueries instead of invalidateQueries for more reliable history
refresh. Add history refetch to SSE onerror handler so dropped connections
don't leave the list stale. Reset page to 0 in HistoryTable when a pending
generation completes.
Closes#231