Skip to content

feat: Models Library with safe downloads, redesigned UI, and multilingual fix - #18

Merged
nikolaykolibarov merged 10 commits into
mainfrom
feat/models-library
Oct 25, 2026
Merged

nikolaykolibarov merged 10 commits into
mainfrom
feat/models-library

Conversation

@nikolaykolibarov

@nikolaykolibarov nikolaykolibarov commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Closes #5

Summary

Replaces the model dropdown and the background download flow with a dedicated Models page where users can compare, download, activate, and delete local transcription models.

Backend

  • One model catalog (internal/models/catalog.go). It holds each model's filename, exact byte size, language support, curated speed and accuracy ratings (1–5), use case, and recommendation. Previously this data was split between transcribe and models. transcribe.Model constants are tested against the catalog.
  • Size fixes. Every size was checked against HuggingFace. The old list had wrong sizes for small.en and medium.
  • Safe downloads (Manager.DownloadModel(ctx, …)):
    • The request is bound to a context, so it can be cancelled.
    • The server-reported size and the received byte count must both match the catalog.
    • The temp file gets a unique name, then is synced, closed, and renamed. This keeps fix(windows): verify and harden the local transcription runtime #14's per-model lock and its Windows close-before-rename fix.
    • Temp files are removed on failure or cancel.
    • IsModelDownloaded requires the exact size, so a truncated file never counts as ready.
  • Downloader (internal/models/downloads.go):
    • Allows one download at a time.
    • Keeps one state, {model,status,downloaded,total,progress,error}, with statuses downloading/cancelling/completed/failed/cancelled.
    • Sends progress events at most every 150 ms, always including the first and last.
    • Rejects cancel requests that don't match the running model.
    • Protects the active model and the downloading one from deletion.
    • Cancels and waits on shutdown.
  • New bindings: GetModelDownloadState, CancelModelDownload, DeleteModel. DownloadModel now checks the request before returning. Finishing a download never activates the model.
  • YAP_CONFIG_DIR keeps dev data separate from the installed app. YAP_MODEL_BASE_URL points downloads at a local mirror for testing failures. Both are documented in DEVELOPMENT.md.

Frontend

  • Models library page, modelled on the Superwhisper library:
    • Compact, sortable table: name, speed, accuracy and size columns. Each row has the OpenAI logo, an EN badge, a Recommended badge, rating bars, and a status icon (download / progress ring / failed / downloaded / active).
    • Header toolbar: search, plus a filter menu (English only, Multilingual, Downloaded, Favorites, with counts) that shows active filters as removable chips.
    • Detail panel: description, type, language, spec icons, ratings, size and status, with one main action (Download · size / Cancel / Retry / Use this model / Active) and a delete that asks for confirmation. It covers the list in small windows, sits beside it in wide ones, and closes with Escape.
    • Favorites: saved in config through the new SetModelFavorite binding.
    • Naming: models are named "Whisper " with an EN badge. Other screens show "(English)" so the variants stay distinguishable.
  • useModelDownload loads the current state on mount, then follows events. Its listener is cleaned up (one listener under StrictMode).
  • Settings: an active-model summary and Browse Models (opens that model's panel) replace the dropdown. Local model controls are hidden when OpenAI is selected.
  • Home:
    • The missing-model prompt downloads that exact model or opens it in the library.
    • Progress is labelled by the model actually downloading, with a Cancel button.
    • The sidebar shows a progress badge.
  • Onboarding reacts only to its selected model, activates it after a successful download, and has a Cancel button.
  • Errors: alert() popups are replaced with inline errors and Retry.
  • Tests: Vitest, Testing Library, and jsdom, run with npm test.

Transcription language fix

whisper.cpp defaults to English, and Yap never passed a language, so multilingual models translated other languages into English instead of transcribing them. For example, Bulgarian came out as "Hello, my name is Daria, today is the time for the Soviet Union…".

  • Yap now sends auto for multilingual models and en for .en models, from the catalog. It goes as the language field on whisper-server requests and as -l for the whisper-cli fallback.
  • I checked this with the pinned whisper.cpp v1.9.5:
    • Bulgarian on Whisper Small now gives "Здравейте, казвам се Дария, днес времето в София е сланчево и топло."
    • English is unaffected.
  • A language picker, so users can pin a language instead of relying on auto-detection, is left for a follow-up PR.

Testing

  • go vet ./..., go test -race ./...
  • cd frontend && npm test (44 tests), npm run build
  • Language tests: catalog → language mapping, the whisper-server request field (including a check that runs on Windows CI), and the whisper-cli -l flag
  • Manual checks with wails dev, an isolated YAP_CONFIG_DIR, and a local mirror that slows some downloads and fails others. Checked in a browser with Playwriter at 850×550 and 1400×900:
    • onboarding: success, failure then retry, and cancel
    • a reload mid-download recovers progress
    • switching pages during a download
    • a second download is refused
    • cancel, then retry
    • failures: HTTP 503, a cut-off download, and a wrong-sized file
    • use and delete, including protection of the active model and the downloading one
    • Settings with OpenAI selected
    • the Home prompt for a missing model
    • quitting mid-download removes the partial file

Note on auto-detection cost

Auto-detection adds a little time to each recording, but only for multilingual models. The default base.en and every .en model are unchanged. Measured with Whisper Small on a 3 s clip:

Backend en auto
Metal 498 ms 529 ms
CPU 1210 ms 1984 ms

The planned language picker will let users pin a language and skip detection.

Development builds can now run beside an installed Yap without sharing
its config, models, history, or logs.
One catalog holds every model's file, exact size, language support,
curated speed and accuracy ratings, use case, and recommendation.
Downloads are context-cancellable and limited to one at a time. Files
are size-verified, synced, closed, and then atomically renamed, and temp
files are removed on failure, cancellation, or shutdown. A wrong-sized
model file no longer counts as downloaded. New bindings expose the
download state and cancel and delete commands. Deletion protects the
active model and the one downloading. Finishing a download no longer
activates it.

Refs #5
A dedicated Models page shows searchable, filterable model cards with
language, recommendation, size, speed, and accuracy. Each card offers
inline download, progress, cancel, retry, use, and delete actions.
Settings shows an active-model summary (hidden for OpenAI), and Home
links into the library and labels progress by the downloading model.
Onboarding reacts only to its selected model. Every download listener is
cleaned up, and browser alerts are replaced with inline errors. Adds
Vitest component tests.

Closes #5
@nikolaykolibarov

Copy link
Copy Markdown
Member Author

Review findings

I confirmed all 9 catalog sizes against HuggingFace with live HEAD requests. go test -race -count=3 and vitest (27/27) pass. No high-severity issues.

  1. Medium: download events can reach the UI out of order (internal/models/downloads.go)
    • Cancel, progress updates, and the end of run each change the state while holding d.mu, but send the event after releasing it.
    • If Cancel races with the download finishing, the UI can receive completed before cancelling. It then stays stuck on "Cancelling" with every Download button disabled until restart.
    • Fix: send events in the same order the state changes.
  2. Low–medium: interrupted downloads leave partial files behind (internal/models/manager.go)
    • Temp files have random names (ggml-x.bin-*.tmp) and are only removed when a download fails or is cancelled normally, or when the model is deleted.
    • A crash, a kill, or the 5 s shutdown timeout leaves a file of up to about 3 GB that is never removed.
    • Fix: remove that model's leftover temp files before each download starts (while holding the model's lock) and when the manager starts up.
  3. Low: onboarding checks an outdated copy of the download state (frontend/src/Onboarding.tsx)
    • After a "busy" rejection, startSelectedDownload decides whether to keep waiting using the state captured when it was created.
    • Right after a remount, that copy can be out of date. Onboarding then stops waiting and never advances when the download finishes.
    • Fix: check a ref that always holds the latest state.

Fixes to follow in this PR.

- Send download events in the same order the state changes, so a Cancel
  racing with completion can no longer leave the UI stuck on
  "Cancelling" with downloads disabled.
- Remove partial downloads left by a crash, kill, or shutdown timeout
  when the manager starts and before each download. Only partials
  untouched for two minutes are removed, so a live download by another
  Yap sharing the directory is kept.
- Onboarding asks the backend for the current download state when its
  request is rejected as busy, instead of trusting a possibly outdated
  render.

Refs #5
@nikolaykolibarov

Copy link
Copy Markdown
Member Author

All three findings are fixed in e35c6e6:

  1. Out-of-order events: a separate emit lock makes every path send its event before the next state change can, so events arrive in the order the state changed. The new test TestDownloaderEmitsInStateOrderWhenCancelRacesCompletion fails without the fix and passes with it (5 race-enabled runs).
  2. Leftover partials: temp files untouched for 2+ minutes are removed when the manager starts and before each download. A newer partial is kept, since it may be a live download by another Yap using the same folder. Covered by TestStalePartialsAreRemoved. Checked in the running app: a stale 5 MB partial was removed at startup and a fresh one was kept.
  3. Onboarding busy check: when its request is rejected as busy, onboarding asks the backend for the current download state. Two new vitest cases cover this.

Re-verified with go test -race ./..., vitest (29/29), npm run build, and the full Playwriter regression against an isolated wails dev. CI is green on macOS and Windows.

…l panel

- Show models in a compact, sortable table (name, speed, accuracy,
  size) with the OpenAI logo, EN and Recommended badges, rating bars,
  and a status column for download, progress, failed, downloaded, and
  active models.
- Move search and a filter menu (English only, Multilingual,
  Downloaded, Favorites, with counts) into the page header. Active
  filters show as removable chips.
- Add a detail panel with description, type, language, spec icons,
  ratings, size, and status, plus one main action (Download, Cancel,
  Retry, Use, or Active) and a confirmed delete. It overlays the list
  in small windows and sits beside it in wide ones. Escape closes it.
- Add favorites, saved in config through SetModelFavorite.
- Name models "Whisper <size>" with an EN badge in the library. Other
  screens show "(English)" so the variants stay distinguishable.

Refs #5
whisper.cpp defaults to English. Yap never passed a language, so
multilingual models translated speech in other languages (e.g.
Bulgarian) into English instead of transcribing it. Send "auto" for
multilingual models and "en" for English-only models, as the language
field on whisper-server requests and as -l to the whisper-cli fallback.
The language comes from the model catalog.

Refs #5
The shell-script worker tests skip on Windows, so add a test that sends the inference request to an httptest server and checks the language field. It runs on Windows CI too.

Refs #5
@nikolaykolibarov nikolaykolibarov changed the title feat: Models Library with safe, cancellable model downloads feat: Models Library with safe downloads, redesigned UI, and multilingual fix Oct 25, 2026
- ConfigManager: guard the config with a RWMutex, apply each setter's
  change and save under one lock, write config.json atomically (temp
  file + rename), and return snapshots from Get. Concurrent favorite
  saves can no longer drop each other or corrupt the config.
- Favorites: keep the optimistic star only while the save is in flight,
  then show the refreshed server state. Ignore repeat clicks while
  saving. If a save fails, open the model's panel with the error.
- Toolbar: let filter chips scroll and the search field shrink, so the
  filter button stays reachable at the 850x550 minimum.
- Escape that the app already handled (cancelling a recording) no
  longer also closes the panel or search. Closing the panel returns
  focus to the row that opened it.

Refs #5
@nikolaykolibarov

Copy link
Copy Markdown
Member Author

Review of the redesign and language fix (cfdcb3f, 4965bb4, 157e141)

An independent review found no high-severity issues. Here's how each finding was handled; fixes are in 74f36e9.

# Severity Finding Resolution
1 Medium ConfigManager had no lock, so concurrent SetModelFavorite calls could drop a favorite or corrupt config.json (a corrupt file is silently replaced with defaults on load) Fixed. The config now has a lock. Each change and its save happen together under it, config.json is written atomically (temp file, then rename), and Get returns a copy. A new -race test runs concurrent saves; it reports a data race without the fix and passes with it.
2 Medium The optimistic star state was never cleared, so the UI could disagree with what was saved Fixed. The optimistic state lasts only while the save is in flight, after which the refreshed server data shows. Repeat clicks are ignored during a save, and the app now waits for the refresh.
3 Medium The header toolbar overflowed at 850×550, hiding the filter button Fixed. Chips scroll and the search field shrinks. Checked with Playwriter at 850×550 with three chips and search open: the filter button stays visible and opens.
4 Low Escape during a recording also closed the panel or cleared search Fixed. The library ignores Escape the app has already handled. Test added.
5 Low A failed star save from the table was silent Fixed. The model's panel opens with "Couldn't update favorites: …". Test added.
6 Low Focus was lost when the panel closed Fixed. Focus returns to the row that opened it. Test added.
7 Low auto may misdetect short clips and costs time Measured, kept. It only affects multilingual models (the default and all .en models are unchanged). Metal +31 ms, CPU +774 ms on Whisper Small. A language picker to pin the language is a follow-up PR.
8 Low Test gaps The language field is now checked on every OS, including Windows CI (157e141). Concurrent saves, in-flight stars and the Escape behaviour are covered by the new tests.

Verification

  • go test -race ./... and vitest (44/44) pass, and the frontend builds.
  • CI is green on macOS and Windows.
  • The full Playwriter regression passed on an isolated wails dev:
    • onboarding; downloading, switching pages, reloading mid-download, cancel/retry
    • no auto-activation; favorites saved and filtered
    • delete protections; HTTP 503 and wrong-size failures; search and sort
  • Bulgarian on Whisper Small now transcribes as Bulgarian.

Show the model name with an EN tag (matching the library) instead of
"(English)", keep the name, tag, and badge on one line, stop the size
from wrapping, and always reserve the space for the downloaded check so
sizes line up. Cards are now keyboard-selectable radios.

Refs #5
- Wrap the cards in a radiogroup labelled by the "Choose a Model"
  heading.
- Name each card "<model> (English)", adding ", recommended" where it
  applies, and describe it with its size, purpose, and whether it is
  already downloaded, so screen readers get what the visuals show.
- Use one Tab stop: arrow keys move the selection and focus between
  cards (wrapping at the ends), and Space or Enter selects.

Refs #5
@nikolaykolibarov

Copy link
Copy Markdown
Member Author

Review of 684247b (onboarding cards)

No blocking issues. All findings are fixed in 77a7c05:

# Severity Finding Resolution
1 Medium role="radio" cards had no radiogroup Fixed. The list is now a radiogroup labelled by the "Choose a Model" heading.
2 Medium The screen-reader name was only the model name, so screen-reader users never heard recommended, size, description, or downloaded Fixed. The name adds ", recommended" where it applies, and size, description and "Already downloaded" are read as the card's description. The decorative check icon is now hidden from screen readers.
3 Low Every card was its own Tab stop, and arrow keys did nothing Fixed. Only the selected card can be tabbed to. Arrow keys move the selection and focus, wrapping at the ends, and Space/Enter selects.
4 Low Test gaps Fixed. New tests cover the group, the names and descriptions, the EN tag, the Recommended badge only on Base EN, and keyboard navigation (46 frontend tests).

Verified in Playwriter at 850×550: the layout is unchanged, and ArrowDown moves the selection and focus.

@nikolaykolibarov
nikolaykolibarov merged commit 43c42c2 into main Oct 25, 2026
10 checks passed
@nikolaykolibarov
nikolaykolibarov deleted the feat/models-library branch October 25, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Models Library - browsable catalog of transcription models

1 participant