Overhaul desktop app: browser capture, redesign, i18n, audit fixes #7

Merged
kleb merged 3 commits from feature/redesign-i18n-audit into main 2026-06-19 03:43:03 +02:00
Owner

Summary

A broad overhaul of the Voice Typer desktop app, spanning the FFmpeg→browser-capture migration plus this round's audit fixes, frontend redesign, model-handling changes, and a new internationalization layer.

Audio / runtime

  • Replace FFmpeg with browser-native Web Audio capture, resampling, and WAV encoding; remove the FFmpeg install/convert paths and validate WAV in the main process.

Audit fixes

  • Parakeet forced language now threads through the worker (it was silently dropped, so the default engine ignored the language dropdown/hotkeys). When a language is forced, the Sherpa language-ID detector is skipped and the result is recorded as forced.
  • Whisper downloads reject truncated transfers (Content-Length check) with an optional sha256 catalog seam.
  • Parakeet cache state is decoupled from the language detector (separate languageIdReady/languageIdSizeMb fields), so a larger un-downloaded detector no longer makes a downloaded model read as uncached.
  • Added main-process tests; refreshed stale FFmpeg docs and removed dead code.

Frontend redesign

  • Regenerated the components/ui/ layer as canonical Radix shadcn/ui; removed Base UI and Phosphor.
  • Warm, single-column layout (Nunito, stone + terracotta theme); dropped the two-pane resizable shell.

Model handling

  • Never auto-download on selection. Selecting a model loads it if cached and otherwise just persists the choice; downloads happen only via the explicit Download control. Startup and reset are load-only too.
  • Model speed/accuracy are shown out of 10; added Whisper large-v2 (non-quant).

Internationalization (English + German)

  • A dependency-free, type-safe i18n layer; the German catalog is typed : typeof en, so key parity is enforced at compile time (256 keys).
  • Localized the renderer, the use-voice-typer hook error messages, and the main-process tray menu (rebuilt on locale change).
  • App-language selector in Settings and the first-run setup.

Validation

bun run typecheck, bun run lint, bun run test (30 pass), bun run knip, and bun run build all pass. Renderer smoke-tested in a browser: English + German render with no leaked keys and no runtime errors.

Notes

  • The repo's format:check fails on pre-existing files from the browser-capture migration (never run through oxfmt); every file touched in this round is oxfmt-clean. A repo-wide bun run format can be a separate follow-up.
  • Out of scope, flagged for later: per-language hotkeys silently change a hidden language under the default Parakeet engine, and there is no engine-aware language validation (Parakeet ~25 vs Whisper 100).
  • Main-process error messages (beyond the tray) remain English; only the tray was internationalized this round.
## Summary A broad overhaul of the Voice Typer desktop app, spanning the FFmpeg→browser-capture migration plus this round's audit fixes, frontend redesign, model-handling changes, and a new internationalization layer. ### Audio / runtime - Replace FFmpeg with browser-native Web Audio capture, resampling, and WAV encoding; remove the FFmpeg install/convert paths and validate WAV in the main process. ### Audit fixes - **Parakeet forced language** now threads through the worker (it was silently dropped, so the default engine ignored the language dropdown/hotkeys). When a language is forced, the Sherpa language-ID detector is skipped and the result is recorded as `forced`. - **Whisper downloads** reject truncated transfers (Content-Length check) with an optional `sha256` catalog seam. - **Parakeet cache state** is decoupled from the language detector (separate `languageIdReady`/`languageIdSizeMb` fields), so a larger un-downloaded detector no longer makes a downloaded model read as uncached. - Added main-process tests; refreshed stale FFmpeg docs and removed dead code. ### Frontend redesign - Regenerated the `components/ui/` layer as **canonical Radix shadcn/ui**; removed Base UI and Phosphor. - Warm, **single-column** layout (Nunito, stone + terracotta theme); dropped the two-pane resizable shell. ### Model handling - **Never auto-download on selection.** Selecting a model loads it if cached and otherwise just persists the choice; downloads happen only via the explicit Download control. Startup and reset are load-only too. - Model speed/accuracy are shown **out of 10**; added Whisper `large-v2` (non-quant). ### Internationalization (English + German) - A dependency-free, **type-safe** i18n layer; the German catalog is typed `: typeof en`, so key parity is enforced at compile time (256 keys). - Localized the renderer, the `use-voice-typer` hook error messages, and the **main-process tray menu** (rebuilt on locale change). - App-language selector in **Settings** and the **first-run setup**. ## Validation `bun run typecheck`, `bun run lint`, `bun run test` (30 pass), `bun run knip`, and `bun run build` all pass. Renderer smoke-tested in a browser: English + German render with no leaked keys and no runtime errors. ## Notes - The repo's `format:check` fails on pre-existing files from the browser-capture migration (never run through oxfmt); every file touched in this round is oxfmt-clean. A repo-wide `bun run format` can be a separate follow-up. - Out of scope, flagged for later: per-language hotkeys silently change a hidden language under the default Parakeet engine, and there is no engine-aware language validation (Parakeet ~25 vs Whisper 100). - Main-process error *messages* (beyond the tray) remain English; only the tray was internationalized this round.
Audio/runtime
- Replace FFmpeg with browser-native Web Audio capture, resampling, and WAV encoding
- WAV validation in the main process; remove FFmpeg install/convert paths

Audit fixes
- Parakeet: thread forced language through the worker (was dropped); skip the
  language-ID detector and record mode 'forced' when a language is forced
- Whisper: reject truncated downloads (Content-Length) and add an optional sha256 seam
- Decouple Parakeet model cached/size from the language detector (separate fields)
- Add main-process tests; refresh stale ffmpeg/docs and dead code

Frontend redesign
- Regenerate the shadcn/ui layer as canonical Radix; remove Base UI and Phosphor
- Warm, single-column layout (Nunito, stone+terracotta theme); drop the two-pane shell

Model handling
- Never auto-download on selection: load cached models, download only on explicit click
- Show model speed/accuracy out of 10; add Whisper large-v2 (non-quant)

Internationalization
- Add a dependency-free, type-safe i18n layer with English + German (key parity
  enforced via `typeof en`); localize the renderer, hook errors, and the tray menu
- Add an app-language selector to Settings and first-run setup
Collaborator

kReview review

Verdict: 1 Low

Overall risk is low; the visible diff mainly changes renderer UI plumbing, with one persistence mismatch in the new locale layer.

1 lower-severity or low-confidence finding(s)

Low (1)

  • Sync renderer locale from persisted settingsui/src/i18n/index.tsx:50
    The renderer can start in the wrong language when the persisted app setting and browser storage disagree, for example after settings.locale was saved as de but localStorage is empty or still en. The new provider initializes only from getInitialLocale() and is mounted above the app without any later sync from the fetched settings.locale, so the settings selector can show one locale while the renderer strings use another until the user changes it again.
    Suggested fix: Use localStorage/navigator only as a bootstrap fallback, then call setLocale(status.settings.locale) when the initial status/settings load completes, or pass the persisted locale into LocaleProvider so it stays aligned with settings.locale.

Excluded as generated or vendored (not reviewed): bun.lock, ui/bun.lock.

Reviewed by kReview at 4e7f6b7c9f. This comment is conservative and based only on the PR diff, metadata, and supplied repository context.

Est. cost ~$4.75 total (797.5k in / 28.8k out) · this run ~$1.63 (272.3k in / 10.6k out) / gpt-5.5.

<!-- codex-forgejo-review --> <!-- codex-forgejo-review-head:4e7f6b7c9feb75e254daed2cf67beeebda1f970c --> ## kReview review **Verdict:** 1 Low Overall risk is low; the visible diff mainly changes renderer UI plumbing, with one persistence mismatch in the new locale layer. <details> <summary>1 lower-severity or low-confidence finding(s)</summary> ### Low (1) - **Sync renderer locale from persisted settings** — [`ui/src/i18n/index.tsx:50`](http://git.kleb.sh/kleb/VoiceTyper/src/commit/4e7f6b7c9feb75e254daed2cf67beeebda1f970c/ui/src/i18n/index.tsx#L50) The renderer can start in the wrong language when the persisted app setting and browser storage disagree, for example after `settings.locale` was saved as `de` but `localStorage` is empty or still `en`. The new provider initializes only from `getInitialLocale()` and is mounted above the app without any later sync from the fetched `settings.locale`, so the settings selector can show one locale while the renderer strings use another until the user changes it again. Suggested fix: Use localStorage/navigator only as a bootstrap fallback, then call `setLocale(status.settings.locale)` when the initial status/settings load completes, or pass the persisted locale into `LocaleProvider` so it stays aligned with `settings.locale`. </details> _Excluded as generated or vendored (not reviewed):_ `bun.lock`, `ui/bun.lock`. _Reviewed by kReview at `4e7f6b7c9f`. This comment is conservative and based only on the PR diff, metadata, and supplied repository context._ _Est. cost ~$4.75 total (797.5k in / 28.8k out) · this run ~$1.63 (272.3k in / 10.6k out) / gpt-5.5._ <!-- codex-forgejo-review-state:eyJoZWFkU2hhIjoiNGU3ZjZiN2M5ZmViNzVlMjU0ZGFlZDJjZjY3YmVlZWJkYTFmOTcwYyIsInN1bW1hcnkiOiJPdmVyYWxsIHJpc2sgaXMgbG93OyB0aGUgdmlzaWJsZSBkaWZmIG1haW5seSBjaGFuZ2VzIHJlbmRlcmVyIFVJIHBsdW1iaW5nLCB3aXRoIG9uZSBwZXJzaXN0ZW5jZSBtaXNtYXRjaCBpbiB0aGUgbmV3IGxvY2FsZSBsYXllci4iLCJmaW5kaW5ncyI6W3sic2V2ZXJpdHkiOiJsb3ciLCJ0aXRsZSI6IlN5bmMgcmVuZGVyZXIgbG9jYWxlIGZyb20gcGVyc2lzdGVkIHNldHRpbmdzIiwiZmlsZSI6InVpL3NyYy9pMThuL2luZGV4LnRzeCIsImxpbmUiOjUwLCJxdW90ZWRfc25pcHBldCI6ImNvbnN0IFtsb2NhbGUsIHNldExvY2FsZVN0YXRlXSA9IHVzZVN0YXRlPEFwcExvY2FsZT4oZ2V0SW5pdGlhbExvY2FsZSk7In1dLCJjdW11bGF0aXZlQ29zdCI6eyJ1c2QiOjQuNzU0LCJpbnB1dFRva2VucyI6Nzk3NDc2LCJvdXRwdXRUb2tlbnMiOjI4ODE4LCJtb2RlbCI6ImdwdC01LjUifX0= -->
- parakeetLanguageIdModelReady now requires a minimum file size, so a truncated
  or 1-byte detector cache no longer reads as ready (which skipped re-download
  and then failed at Sherpa init). Update the readiness test accordingly.
- use-browser-recorder: stop the acquired MediaStream tracks if AudioContext
  wiring fails before activeRef takes ownership, so the microphone is released.
- use-browser-recorder: track a pending start session id so a stop/cancel that
  arrives during async mic startup aborts the in-flight start instead of
  installing a live recording the main process already considers ended.
- parakeetLanguageIdModelReady now derives a per-file minimum from the bundle
  size (each file must be >= 20% of its expected share), so a detector cache
  truncated to e.g. 128 KB no longer reads as ready and skips re-download.
kAgent left a comment

Verdict: 1 Low

Inline findings for 4e7f6b7c9f are anchored below; the review summary comment has the full report.

<!-- codex-forgejo-review:inline --> **Verdict:** 1 Low _Inline findings for `4e7f6b7c9f` are anchored below; the review summary comment has the full report._
@ -0,0 +47,4 @@
const LocaleContext = createContext<LocaleContextValue | null>(null);
export function LocaleProvider({ children }: { children: ReactNode }) {
const [locale, setLocaleState] = useState<AppLocale>(getInitialLocale);
Collaborator

Low: Sync renderer locale from persisted settings

The renderer can start in the wrong language when the persisted app setting and browser storage disagree, for example after settings.locale was saved as de but localStorage is empty or still en. The new provider initializes only from getInitialLocale() and is mounted above the app without any later sync from the fetched settings.locale, so the settings selector can show one locale while the renderer strings use another until the user changes it again.

Suggested fix: Use localStorage/navigator only as a bootstrap fallback, then call setLocale(status.settings.locale) when the initial status/settings load completes, or pass the persisted locale into LocaleProvider so it stays aligned with settings.locale.

**Low: Sync renderer locale from persisted settings** The renderer can start in the wrong language when the persisted app setting and browser storage disagree, for example after `settings.locale` was saved as `de` but `localStorage` is empty or still `en`. The new provider initializes only from `getInitialLocale()` and is mounted above the app without any later sync from the fetched `settings.locale`, so the settings selector can show one locale while the renderer strings use another until the user changes it again. Suggested fix: Use localStorage/navigator only as a bootstrap fallback, then call `setLocale(status.settings.locale)` when the initial status/settings load completes, or pass the persisted locale into `LocaleProvider` so it stays aligned with `settings.locale`.
kleb merged commit 8f17f301ae into main 2026-06-19 03:43:03 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kleb/VoiceTyper!7
No description provided.