3c33aa62b83c0f334e4ce65ca650d243a953e935
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e66bd218d9 |
fix(audio): prevent dictation hangs on stalled microphones (#974)
* fix(audio): bound capture reads and support cancellation * fix(recognition): bound and serialize capture shutdown * fix(recognition): cancel retries and report stalled microphones * fix(audio): prefer pulse when the ALSA default is unavailable * fix(recognition): guard late callbacks and finalize capture tests * docs(audio): explain input selection and stall recovery * fix(speech): enforce auto-pause across buffered reload timeouts * fix(speech): recover auto-resume after reload timeout * test(speech): fix auto-pause follow-up test regressions * fix(speech): retain auto-resume after reconfigure timeout * fix(tray): preserve auto-pause recovery across suspend * test(capture-shutdown): give the bare tray the suspend path's shortcut managers _on_system_suspend now releases the main and per-language hotkey listeners before sleep (#1025); the fixture builds TrayIndicator without __init__, so it needs the two attributes present. * fix(tray): release hotkey listeners on suspend even while auto-paused Merging #1025's suspend cleanup with this branch's auto-pause handoff left an early return that skipped shortcut_manager.stop() whenever a configured game was active, keeping EVIOCGRABs held through sleep. The paused branch now only owns the model-lifecycle work; listener release and stopping active recognition run for every suspend. * fix(tray): hold the apply lock only around the download's verify and save Wrapping the whole recommended-model download in _apply_settings_lock blocked every GTK-thread settings apply (e.g. Test Dictation) for the length of the transfer. The engine already serializes reconfigures, so the lock only needs to cover verifying the download still owns the receipt and saving it; an apply that lands mid-download now proceeds instead of freezing the app. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
3e4230ffc2 |
fix(settings): make model downloads explicit and cancel the whole intent (#1006)
* fix(settings): make model downloads explicit and cancel the whole intent "Use it" on the recommendation card applied a different model than the one shown, a cancelled or failed apply left the staged model behind so the next Test Dictation downloaded it, and cancelling a download tore down and then reloaded the previous model while the dialog waited. - Recommendation is now passive text, compared by size tier, with a link to docs/MODEL_SELECTION.md. The "Use it" button is gone. - Every Settings download asks first and shows size and source host. Declining, cancelling or failing drops the staged model spec and puts the pickers back on the saved config. - Test Dictation refuses a model that is not on disk instead of applying it. - reconfigure() fetches a missing model before releasing the loaded one, so a cancel restores state without reloading or rehashing the old model. Cancellation state is reset only when a download claim is taken or ended. - Follow-layout mode shows the resolved language in the simple Main language control, read-only, and simple edits keep the "layout" setting. Fixes #979 Fixes #980 Fixes #981 Fixes #982 Fixes #985 Fixes #988 * fix(settings): restore follow-layout mode when a model change is declined * fix(settings): keep follow-layout and dictation consistent during model changes * style(tests): format follow-layout regression tests * fix(recognition): skip resume reinit while a model download is running * fix(recognition): keep a lazy reload pending when resume is skipped during a download --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
329a7d7f6a |
fix(settings): stage Advanced model spec until Download is confirmed (#908)
* fix(settings): stage Advanced model spec until Download is confirmed Picker changes in Speech Model -> Advanced staged a spec but immediately triggered a model download for every intermediate selection. Gate the auto-apply download path so a missing model only stages: a Download action on the model info card now runs apply_settings() as the single explicit confirmation. Simple-mode questions keep the instant apply-and-download flow. Fixes #894 * fix(settings): repaint model info card after a completed download The post-download paths repopulated pickers and resynced the engine but never repainted the info card, so a finished download left the amber status and the Download action visible for a model already on disk. The cancel path already repainted via the idle resync; give success its own. * fix(settings): keep staged spec through picker rebuilds and save non-model edits * fix(settings): persist non-model edits without a model-reloading reconfigure * fix(settings): live-apply non-model edits, keep whispercpp params saved-only while staged * fix(settings): live-set changed VAD/timeout attrs without reconfigure while staged --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9898bb8dd9 |
feat(ui): in-app Dictation Pad for Wayland-safe dictation (#887)
* feat(ui): add in-app Dictation Pad for Wayland-safe dictation Dictated text can now be routed into a text box inside Vocalinux's own window instead of being injected into other applications — an explicit fallback for desktops where cross-app injection is unreliable or blocked (notably Wayland). The pad opens from the tray menu, captures dictation when enabled via Settings or its own checkbox, and lets the user copy text out by hand. 'delete that' removes the last segment in-pad. Closes #726. * fix(ui): consult live config for pad auto-reveal and Copy All The reveal gate used the controller's enabled flag, which only syncs on pad show or its own checkbox — enabling capture via the Settings switch while the pad was closed silently buffered dictation with no reveal. _apply_append now reads dictate_to_pad from the shared config. Also: Copy All copies the widget text (dictation plus in-pad edits), and re-showing the pad no longer does a wholesale set_text — the view stays incrementally in sync, so manual edits survive hide/reopen. * fix(ui): keep dictation pad capture and commands inside the pad Address all Greptile findings on PR #887: - Read the capture toggle from the shared ConfigManager instead of re-parsing config.json per segment, so a torn mid-write read can no longer fail-open and send dictation to the focused app - Sync edits made directly in the pad widget back into the controller, keeping delete semantics consistent with the visible text - Stamp queued widget operations with a generation tag so a Clear drops appends still sitting in the GTK idle queue - Route undo/redo, select all/line/word/paragraph and cut/copy/paste to pad-local implementations while capturing, so their shortcuts never reach whichever application holds focus - Track where the last segment was delivered so "delete that" follows the text rather than the current capture toggle - Annotate every new signature and add callback-level tests that pin the routing, queue, and undo behavior * fix(ui): keep pad controller, queued ops, and delete target in sync Manual edits could overwrite queued dictation in the controller because GTK input events outrank idle callbacks; the changed handler now replays pending widget ops before syncing the view. The controller also tracks dictated segments across undo snapshots, so a pad undo pops the "delete that" target instead of leaving a stale length. Catches in the pad are narrowed to the GTK/config error set per repo style, and missing type annotations are added. * fix(ui): close flush races in pad idle queue and delete retarget Flushed idle ops stayed scheduled with GLib and ran a second time, duplicating appends; each queued op now only acts while its entry is still pending. A queued full-view refresh could also erase a manual edit that landed first, so the changed-handler flush now drops stale set-text ops instead of replaying them. And clearing the pad destination on undo-to-empty kept a redo from restoring the "delete that" target; the destination flag is preserved so redo re-arms it. * fix(ui): replay pad idle ops before edits land, not after Flushing in the changed handler still let queued appends leapfrog the keystroke that fired it, and a stale dispatch could cancel a later queued op with identical text. Pending entries now match by identity, and the replay moved to insert-text/delete-range — signals that fire before the edit is applied — so queued dictation and deletes keep true chronological order against manual edits. * fix(history): target late segments to their own snippet by id A straggler from a just-ended session could merge into whichever snippet was newest when it arrived, and audio captured before a session opened was attributed to that session while decoding on its worker. - TranscriptionHistory.add returns the snippet id and the new extend_entry() extends a specific entry, replacing the positional extend_latest target in the late-segment path - session_started_floor stamps when a session opens; in-session attribution also requires the capture to postdate it - test_capture_ceiling closes the mic-test window at callback restore so test speech still decoding can no longer leak into history * fix(settings): keep deferred edits from overwriting a newer choice The deferred-persist filter treated "config still equals the baseline" as untouched, so a reopened dialog that applied a key back to its baseline value could still lose it to the closed dialog worker. - Record the generation of the apply that last wrote each advanced key; a pending edit now survives only when no newer apply wrote that key - A worker apply whose snapshot a newer apply predates now skips its reconfigure and save entirely, so collection order — not lock order — decides which snapshot wins * fix(settings): skip stale applies before stopping dictation The superseded-apply check ran after stop_recognition, so a stale worker still interrupted a live dictation session. Recognition now stops inside the apply lock, after the generation check. * fix(shortcuts): keep language edits from breaking dictation Two per-language shortcut fixes (#805): - A binding edit landing while a session is live no longer stops recognition. The dialog's per-keystroke persist still saves the new binding, but the tray defers the listener rebuild to the next IDLE transition, so a held push-to-talk keeps its release callback and a toggle session is never cut off mid-utterance. - A row edited onto a key another row holds can no longer displace that row's binding. Each row's remembered shortcut is reserved for its own row before claims run in row order, so the stealing edit is rejected and the held binding survives. - Persists that resolve to the already-saved bindings (mid-edit keystrokes, rejected duplicates, reverted fields) skip the save and the listener rebuild entirely. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
5c031e64fb |
feat(settings): bilingual dictation support with deferred settings edits (#424)
* feat: add configurable Whisper language candidates * fix(settings): defer language candidate edits and tolerate list config The candidates entry applied settings on every keystroke, restarting the whisper.cpp model per character, and edits typed while an apply held the guard were dropped so the saved value could differ from the entry. Defer application like the initial prompt: mark edits dirty and apply on page change or close; _finish_auto_apply re-applies edits that landed mid-apply, and _persist_pending_text_edits saves them if the dialog closed first. A JSON list in config also crashed the dialog on open because Gtk.Entry.set_text needs a string; populate the entry through the engine's _normalize_language_candidates so both stored forms render the same text. * fix(recognition): validate whisper.cpp language candidates before selection auto_detect_language's probability map names every language the model can return, but candidate selection scored configured codes against it with a 0.0 default, so a typo or "auto" could still win the max() and reach transcription. Only codes present in the probability map are eligible now; unsupported codes are logged and skipped, and when none qualify the unrestricted detection result is used instead of forcing a code the model never scored. Also add the missing input annotation on _normalize_language_candidates. * fix(settings): persist deferred text edits off the GTK main loop _finish_auto_apply runs on the main loop and called _persist_pending_text_edits synchronously, so reconfigure() restarted the model in-process and froze the UI when the dialog closed mid-apply. Run the reconfigure + config save on a worker thread, matching the normal apply path. Also annotate _on_language_candidates_changed's signature. * fix(settings): guard deferred saves and annotate new candidate tests * fix(settings): reconcile the engine to winning settings after a mid-restart apply * fix(settings): drop superseded deferred edits instead of saving them A dialog closed mid-apply stashes its pending text edits for a worker to persist after the running apply finishes. The worker's mtime check could not tell the in-flight apply from a newer one started in a reopened dialog, so the older snapshot could reconfigure the shared engine and save over the settings that won. Serialize applies and the deferred persist on a shared lock and order them with a generation counter bumped when an apply begins. The worker now skips reconfigure and the save entirely when a newer apply has begun, and skips the restart when the running apply already landed the same values. * fix(settings): merge deferred text edits per key instead of dropping them Closing the dialog mid-apply stashes the typed prompt/candidate values with the apply's generation. When a reopened dialog applies newer settings first, the generation check used to discard the whole stash — losing the closed dialog's edits even where the newer apply never touched those keys. Persist keeps a pre-edit baseline per key: after a newer apply lands, only keys it actually changed are dropped; the rest still reconfigure the engine and save, so a stale value can never reach the shared engine while untouched edits are no longer lost. * refactor(settings): narrow deferred-edit persist exception with context * chore: retrigger ci --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9646011e77 |
fix(settings): speech model follow-ups from QA (#836)
* fix(settings): speech model follow-ups from #834 Keep the Unused downloads expander in sync with search, recommend Faster Whisper .en weights for English, parse nvidia-smi MiB VRAM, stop simple mode from clobbering Faster Whisper/Parakeet, and ceil RAM so ~7.4 GiB counts as 8. * fix(settings): keep FW .en picker ids and test unused search Represent Faster Whisper English catalog variants in the Settings size combo so tray-saved small.en is not rejected and overwritten. Add behavioral unused-downloads search coverage for match/nonmatch/clear. |
||
|
|
7664266823 |
feat(commands): localize punctuation voice commands (#640) (#642)
* feat(commands): localize punctuation voice commands by language Voice command phrases were English-only even when recognition language was set to Italian, French, etc. Merge locale punctuation/line-break aliases from the recognition language into CommandProcessor, keep English phrases available, and document the behavior. Fixes #640 Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(commands): keep line breaks and French apostrophe aliases Attach-left punctuation now eats only horizontal whitespace, so a localized "new line" followed by a period or comma still inserts a newline. French aliases include ASCII, U+2019, and backtick apostrophes, and longest-match still wins so "point" cannot fire inside "point d'interrogation". --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> |
||
|
|
affbf6a9af |
feat(settings): simple setup for the speech model, with Advanced as an island (#801)
* feat(settings): make the language picker searchable while its list is open Entry completion only ever covered typing into the closed entry. Open the dropdown and the keyboard goes to GTK's own first-letter jump, so nothing typed reaches a filter, and with thirty-odd languages scrolling is the only way through. The first attempt at fixing this (a filtered ComboBox model) had the same limitation for the same reason. Replace the combo with a picker that opens a popover holding a search entry, focused on open, above the list. Every keystroke narrows the rows, Enter takes the first match, a click takes that row. It keeps the GtkComboBoxText methods the dialog calls, mirrors the store's column order in base_model for the shared row helpers, and emits "changed" only when the selection actually changes, so re-selecting the saved language on open is not taken for a user edit. Visibility uses the matcher the typed-text resolver already uses, so the list and Enter cannot disagree. The completion helper and the combo styling skip the picker: it filters its own list, and the ComboBox-only calls do not exist on a Box. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * feat(settings): add a simple mode that derives the model from the language The Speech Model panel asked four questions, but only two of them can be answered without knowing the hardware: what language you speak and whether you want it faster or more accurate. Size follows from the machine and the specialization follows from the language, and the app already computed both — it just printed the answer beside the pickers instead of using it. Simple mode asks for the main language, whether whole texts are also dictated in other languages, and a priority. There is deliberately no second language field: whisper takes one language or none, so a pair cannot be passed, and with a language pinned the multilingual weights already handle inserted foreign words. The switch is the "none" case. Priority moves relative to what hardware detection recommends rather than naming an absolute size, so "most accurate" means something different on a laptop than on a workstation. Opening simple mode reads the priority back from the size already configured, so switching modes never changes the loaded model on its own. Simple mode steers the existing controls rather than writing the configuration itself, so applying, downloading and the info card keep going through one path. Advanced mode is the previous panel, unchanged. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): stop simple mode reloading the engine four times per pick Reported against the installed build: choosing a language locked the window for a moment. Simple mode steers the engine, language, size and variant controls, and every one of those change handlers ends in _auto_apply_settings, so a single pick reconfigured the speech engine up to four times on the UI thread, reloading the model each time. Hold a guard across the steering and apply exactly once when it is done. The simple language box was also a plain list with no entry, so it could not be typed into — a step backwards from the advanced row, and unusable with over thirty languages. It now uses the same searchable combo, with the same commit and restore behaviour for text that resolves to nothing. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * refactor(settings): make simple the panel and Advanced a reveal The Simple/Advanced dropdown framed the two as alternatives, which put a mode decision in front of the user before any of the questions they can answer. They are not alternatives: simple is the panel, and advanced is more detail about the same choice. The mode selector is gone. The simple questions are always on screen, with an Advanced switch that slides the engine, size and specialization rows out underneath. Because simple mode steers those rows, they also work as a readout of what the simple answers resolved to, which the old either/or hid. The stored settings_mode becomes a show_advanced flag, remembering whether the detail was left open. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * refactor(settings): second language list, and Advanced in its own window Reworked to what the panel is actually for. The main language comes from the keyboard layout. Turning on "I also dictate whole texts in other languages" reveals a second list rather than silently flipping to detection, so the choice is visible. Advanced is a button that opens the detailed controls in their own window, instead of an inline reveal that pushed the page around. Naming a second language resolves to automatic detection, because whisper takes one language or none. The one case where both can still be honoured is two English entries — en-US plus en-IN — where English stays pinned and the English-only weights, same size and better at English, remain available. Also fixes a name collision the reveal had: advanced_revealer already belonged to the power-user toggle on the Advanced page, and the section built later won the attribute. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * feat(settings): use the filtering picker for both simple language lists The main language and the second language are the same thirty-odd-row lists as the advanced one, so they get the same dropdown filtering rather than a completion popup that only reacts while the entry has focus. Refs #779 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): make simple mode do what it says on screen Four things reported from the installed build, all real. The second language list never appeared: the row is flagged no_show_all, and show_all() is a no-op on such a widget, so the flag has to be cleared first — the dance _set_custom_shortcut_row_visible already does. There was no multilingual answer. "Any language (auto-detect)" is now the first entry of the second list and the default when the switch is turned on, so the switch alone visibly does something. Changing the priority showed nothing, because the info card — the only surface that says which model was picked and what it costs — had been moved into the advanced window. It is back on the page under the simple questions. Opening Advanced crashed the compositor. KWin 6.7.4 (Wayland) died with unbounded recursion in KWin::Window::findModal(), a cycle in its transient-for chain, when the advanced window — a long-lived toplevel transient for this dialog, hidden on close and shown again with present() — was opened. Coredumps at 2026-09-06 20:20:10 and 20:39:49; the crashing state is kept as tag kwin-crash-repro. The window now stands on its own, is built on every open and destroyed on close with the controls re-parented out first. KDE Bug 483265. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): bring the advanced window up in front of the dialog Reported: it opened behind Settings. Dropping the transient parent (which crashed KWin) also dropped the only reason the compositor had to stack it over the dialog, and on Wayland a toplevel mapped without an activation token lands behind the focused window. Present it with the timestamp of the click that opened it: that asks the compositor for focus and stacking, and needs no transient link. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * refactor(settings): Advanced as an island under the simple card, not a window A second toplevel went wrong twice on KWin 6.7 / Wayland. Transient for the dialog it crashed the compositor (unbounded recursion in KWin::Window::findModal, tag kwin-crash-repro, KDE Bug 483265). Standing on its own it could not be raised above the dialog at all, not even by hand. Advanced is now its own card under the simple questions: collapsed to a header by default, expanding in place, remembered in show_advanced. Both cards are on screen at once, so the expanded rows double as a readout of what the simple answers resolved to, and a change made in the advanced rows is read back into the simple ones — skipped while simple mode is itself steering them. The info card sits between the two, where a priority or language change shows its effect and its cost. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): let a same-size weight on disk stand in before downloading Reported: turning the "I also dictate whole texts in other languages" switch off went from base to base.en — same size, different weights — and sat the user through a modal 141 MB download for it, with the window blocked behind the progress dialog. Turning it on had done the same in the other direction. Simple mode now looks at the disk before settling on a variant. A downloaded weight of the same size that can serve the language stands in: English-only first when English is wanted, the plain multilingual next, quantized ones last. English-only weights never stand in for another language or for detection. When nothing of that size is on disk the derived variant stays and the download runs as before. The info card still offers the better variant to download; it just no longer forces it. Refs #779 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(settings): keep Advanced language pin over stale simple multi _sync_simple_from_advanced restored the other-languages switch and persisted simple_second_language even when Advanced had a pinned language. The next simple edit then decoded to auto and silently replaced the pin. Clear/resync simple multi on a pin, refresh that readout when Advanced language changes, and drop the second-list Enter hook that double-applied through SearchablePicker. Refs #779 * fix(settings): keep two-English simple multi across Advanced pin sync Clearing every leftover second language on a pinned Advanced readout was too broad: en-US plus en-IN still pins English, and wiping that answer forgot the second English choice on the next open. Restore multi only when the stored second would keep the pin; otherwise drop it so a later simple edit cannot decode to auto. Refs #779 * fix(settings): restore simple multi only when it matches live language The pin-side keep left the auto branch restoring any stored second. Two English over Advanced auto still decodes to en-us, so the next simple edit silently replaced detection. Keep leftover multi only when it still decodes to the live Advanced language either way. Refs #779 * fix(settings): keep language-change tests honest after the main merge Mock dialogs treated _initializing as truthy and skipped the handler. Also call _refresh_simple_readout explicitly so the simple card updates. * fix(settings): type the language-entry GTK callbacks Greptile flagged missing param and return annotations on the new activate and focus-out handlers. Match the surrounding Any/bool style. Refs #779 * fix(settings): sort imports for isort after Parakeet merge --------- Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
8f4aeca2f1 |
feat(asr): add Parakeet TDT 0.6B engine via sherpa-onnx (#802)
* feat(asr): add Parakeet engine via sherpa-onnx Adds a "parakeet" engine backed by sherpa-onnx, running NVIDIA Parakeet TDT 0.6B int8 models on CPU: v3 (25 European languages, auto-detected) and v2 (English). Output is punctuated and capitalized. On an Intel Core Ultra 7 155H it decodes at about 12x realtime on CPU (RTF 0.08), roughly 8x faster than whisper.cpp large-v3-turbo over the same clips with its Vulkan GPU backend (RTF 0.59). Being a transducer it also has no 30s window to pad, so a short push-to-talk utterance returns in ~0.3s instead of ~2.4s. Follows the existing engine layout: utils/parakeet_model_info.py holds the catalog and the get_model_path / is_model_downloaded / list_downloaded_models / delete_model helpers, while _init_parakeet / _transcribe_with_parakeet / _download_parakeet_model follow the same shape as their whisper.cpp counterparts and reuse _stream_model_download for progress and cancellation. Registered in the engine dispatch, the settings engine/model/language pickers, the model info card, the download prompts, the unused-downloads list and install.sh. Models download to <models_dir>/parakeet/<model>/ via temp files renamed on success; is_model_downloaded requires all four files, so an interrupted download is retried rather than loaded. Requires the new "parakeet" extra (sherpa-onnx). * fix(install): write only the engine key for parakeet The parakeet branch duplicated a full default config.json. ConfigManager deep-merges DEFAULT_CONFIG on load, so every other key was redundant: a config holding only "engine": "parakeet" still resolves parakeet_model_size to v3-european along with the VAD, shortcut and text-injection defaults. * fix(install): keep push-to-talk defaults and switch existing configs Writing only the engine key left config.json without a shortcuts section, which _migrate_shortcuts_config reads as a pre-push-to-talk upgrade and pins back to ctrl+ctrl/toggle - contradicting the installer's own "Hold Right Alt while you speak" message. Seed that section too. Point the config at parakeet with set_configured_engine rather than only on first create, so --engine=parakeet also switches a machine that already has a config.json, and give print_welcome_message a parakeet arm so the summary is not blank. The interactive picker overwrites SELECTED_ENGINE, so document the flag combination that reaches this branch. * fix(asr): pin Parakeet bundles and report one progress bar Pin each bundle to a Hugging Face commit instead of resolve/main, so a file replaced upstream cannot change what an install downloads. This is the same approach whispercpp_model_info takes with its pinned revision. Remap each file's progress into its slice of the bundle. The four files were reported through the single-file downloader independently, so the fraction ran to 1.0 and restarted four times; it now advances once and names the file being fetched. * fix(asr): reject a truncated Parakeet download _stream_model_download only rejects an empty body, so a truncated but nonempty response was renamed into the model directory and then reported as installed, because is_model_downloaded checks existence. Record the byte size of each file at the pinned revision and check it before the rename, the same order whisper.cpp verifies its digest in. A short file is a truncated transfer rather than an upstream change, since the revision is pinned, so it raises and the temp file is removed. * style(asr): annotate the Parakeet model catalog mypy inferred the catalog values as object, so reading file_sizes out of an entry raised attr-defined and no-any-return under `just typecheck`. * fix(asr): pin Parakeet digests in model_checksums.txt AGENTS.md requires every speech model to be verified against a pinned digest before install, failing closed on an unpinned one. Parakeet was checking only the byte size. Teach scripts/generate-model-checksums.py to emit Parakeet entries: the ONNX files are LFS so Hugging Face publishes their sha256, and tokens.txt is hashed from the downloaded bytes the way VOSK archives are. Several bundles ship the same file names, so a key carries the model name. _download_parakeet_model now calls verify_model_file before the rename and catches ChecksumError alongside the other download failures, matching the whisper.cpp downloader. * fix(asr): verify on-disk Parakeet models before load Existing bundle files were trusted by path alone, so a leftover or copied-in tree could reach sherpa-onnx unchecked. Hash each file against its pin on download skip and on init, and cover the Parakeet pins in CI. * fix(ui): hide Parakeet language picker that ignores the setting Parakeet language coverage is the model choice, not a catalog language. Hide the picker and persist auto so Settings does not write a lying value. * fix(asr): normalize Parakeet language to auto at startup CLI --language and stale saved languages still reached SpeechRecognitionManager even though Parakeet ignores catalog language. Force auto on the shared helper used by main and the manager so stored settings stay honest. * fix(ui): restore language after leaving Parakeet Remember the prior Whisper/cpp language across a Parakeet visit so switching back does not leave recognition stuck on auto. Keep Parakeet forced to auto and the picker hidden. Also satisfy black on the startup normalizer path. * fix(ui): keep language memory across Vosk fallback Vosk's unsupported-language en-us coercion was writing into _last_non_parakeet_language, so Whisper→Parakeet→Vosk→Whisper restored en-us instead of the user's Greek (or other) preference. Remember the pre-coercion catalog language and prefer it when leaving Vosk. * fix(ui): keep language memory when entering Parakeet from Vosk Vosk's coerced en-us fallback was still written into _last_non_parakeet_language on Parakeet entry, so Vosk→Parakeet→Whisper restored en-us instead of the pre-coercion preference (Greek or auto). Share one coerced-fallback predicate across entry, leave-Vosk, and language sync, and cover those paths with regression tests. * fix(tests): set initializing flags false on language-change stubs Plain Mock attributes are truthy and were short-circuiting the handler. --------- Co-authored-by: Alex Thunder <AlexThunder@gmail.com> |
||
|
|
40bc31bebf |
fix(settings): stop freezing when applying an already-downloaded model (#790)
* fix(settings): apply already-downloaded models off the GTK main loop Changing Model Size, Specialization, or Language froze Settings when the model was already on disk, because reconfigure ran in the changed handler. Move that apply onto a worker thread (same shape as the download path) and keep the apply-guard held until GLib.idle_add clears it so a second pick cannot start a parallel load. Fixes #789. * fix(settings): guard Test/apply while downloaded-model worker runs Block apply_settings and Test Dictation while _applying_settings is held so a concurrent Test click cannot race the shared engine. Annotate the new worker/helpers per AGENTS.md and cover the no-op/block behavior. * fix(settings): resync pickers when a second pick lands mid-apply While an already-downloaded model worker was running, Model Size / Specialization / Language could still move. The apply guard blocked saving the new pick, then finish only cleared the flag, so the UI could show B with dictation still on A. On finish, if the selection no longer matches the saved config, put the pickers back. Idle resyncs from workers now skip a destroyed dialog the same way update check does. * fix(settings): always resync pickers after model apply finishes A second Model Size pick on whisper.cpp could move the size combo while the apply guard skipped rebuilding specialization, so selected settings still looked like the saved variant and finish skipped the resync. Always put the pickers back on the saved config when the worker ends, and restore language from that config too. * test(settings): prove mid-apply picker handlers round-trip on finish Call the real size, specialization, and language handlers while the apply guard is held, and run finish through the real resync path so whisper.cpp size and language cannot silently stick on the second pick. |
||
|
|
6b14338d1e |
feat(tray): offer the recommended model from the missing-model notification (#687)
* feat(tray): offer the recommended model from the missing-model notification Dictation without a model on disk showed a dead-end notification telling the user to open Settings, find the model picker and start the download by hand, at the moment they wanted to talk. There was no Download button anywhere: downloads only happen as a side effect of changing a selection. The notification now carries a "Download <model> (<size>)" button for the model the picker already stars for this machine, so recommendations stay in one place: get_recommended_model() for whisper.cpp, including the English-only variant when an English language is selected, and the RAM based recommendations for VOSK and Whisper. Clicking it downloads in the background, reports progress in the notification, and saves the model for that engine only after it loaded. Notification servers that do not advertise the actions capability, and systems without libnotify, still get the plain text they got before. Dictation is not started for the user afterwards: a download can take several minutes, and grabbing the microphone unannounced would surprise someone who has long since moved on. * feat(tray): actually fetch the offered model, and only claim it when loaded Review follow-ups on #687. reconfigure() only re-initializes when engine, model or language change, and force_download is read during that re-init. First-run defaults are whisper_cpp + tiny, and tiny is what get_recommended_model() returns on a CPU-only machine under 16GB, so the click was a no-op that logged "Applied VAD/silence timeout changes.", saved the model, said "Speech model ready", and left the next shortcut showing the same missing-model notification. Add force_reinit to reconfigure() and pass it here, then gate the save and the success notification on speech_engine.model_ready instead of on reconfigure() not raising. The "starting..." notification is now created in _download_recommended_model, which runs on the main loop where libnotify dispatches the action; only the handle travels to the worker, which already routes update/close/notify through _idle_once. The offer body no longer promises a button the server cannot draw: with no action it points at Settings, like the notification it replaced. Tests cover the case that breaks the feature — the recommendation equal to the configured model — behaviourally, through a stand-in engine that gates re-init the way reconfigure() does, plus the real reconfigure() contract in both directions. They also stop touching sys.modules at collection time, which is the desync #686 hit, and black is clean. * test(notifications): cover the libnotify/notify-send boundary directly Every test that touches notifications so far patches the whole module away, so the module itself had no coverage: the fallback to notify-send when the server has no action support or libnotify fails to initialize, the handle kept alive in _pending until the user answers, and update() refusing a fallback handle. These drive it with a fake Notify installed through the module's own lazy cache rather than by patching gi.repository, which is shared with every other test in the process. * fix(download): allow only one model download at a time across tray and Settings The tray's download worker and the Settings dialog's both drive the same engine, which has a single progress callback slot and a single configuration. Started together, the later one overwrote the earlier one's progress callback, the earlier one's finally block then cleared it mid-download, and whichever finished last became the persisted model. Give the engine a non-blocking claim. Both Settings entry points and the tray take it before starting a download and release it when the worker ends; whoever loses the race is told a download is already running and the Settings picker goes back to the saved model. --------- Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com> |
||
|
|
a4bd398945 |
fix(settings): save the model only once the engine loaded it (#685)
* fix(settings): save the model only once the engine loaded it Applying settings wrote the config first and reconfigured the engine afterwards, and reconfiguring is what downloads a missing model. A download that failed, was cancelled, or was interrupted therefore left the config naming a model that never reached the disk: the next start reported "No Speech Model" while the previously working model sat on disk, deselected. Reconfigure first and persist only on success, so a failed switch keeps the settings that are actually running. On failure the pickers are put back on the saved model. They otherwise kept showing the rejected one, which also blocked a retry, since re-selecting the same entry emits no "changed" signal. Selecting an engine now applies as well. Every other control on the page applies on change, but the engine only repainted the pickers: the config and the recognizer stayed on the previous engine until another control was touched, and closing the dialog dropped the change without a word. Remote API is deferred until its server URL is filled in, since applying it empty can only fail validation. * fix(settings): restore the engine picker after a failed engine switch Fixes the known limitation called out in the previous commit (#692): when applying a newly selected engine failed, the resync restored the model pickers but left the engine combo displaying the engine that was never applied, so the page described a state that was not running. Set the combo back to the saved engine while holding the apply suppression flag, so _on_engine_changed() repaints the pickers without cascading into another apply or download prompt. * fix(settings): resync the engine picker on every path that never applied Review follow-ups on #685. #692 was not actually closed. The resync lived only in the download thread's except block, and _apply_settings_internal() reports failure by returning False as well as by raising, so the thread fell through to set_complete(True, "") and reported a switch that never happened. Act on the return value instead of depending on #690 to make that except live, and add the same handling to apply_settings()'s modal path, whose except never resynced at all. Add _resync_engine_ui_if_unapplied() as the backstop the review asked for: after the modal closes, and when the dialog closes, compare the engine on screen against the engine in the config and restore the picker when they disagree. That also covers Remote API left without a server URL, where applying stays deliberately deferred. _on_engine_changed now checks _initializing / _applying_settings itself, like every other control on this page, so the combo move made by a resync repaints without rewriting the user's language or cascading into another apply. Tests no longer touch sys.modules at collection time: the reimport moved into a module-scoped fixture that also restores the attribute on vocalinux.ui, which is what desynced test_model_deletion on 3.9/3.10. New cases cover the modal path that #692 is about — needs_download true with the apply returning False, and with it raising "Download cancelled" — plus the close hook. The stub now uses RecognitionState.IDLE rather than the string "idle", so tests stop taking the stop_recognition branch and sleeping 0.5s. |