* 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>
* feat(recognition): cancel in-progress transcription with hotkey
Adds per-session cancellation across the manager, tray, and Test Dictation.
Fixes#991
* fix(settings): keep Test Dictation cancellation responsive
Fixes#991
* fix(recognition): keep late workers cancellable (#991)
* test(tray): avoid early tray import in cancellation tests
* fix(recognition): keep a cancelled capture thread out of the next session
A cancel returns to IDLE without joining the capture thread, so a thread
still in its reconnection backoff could wake inside a newer recording,
reopen that session's device and write into its buffer. Tie the capture
loop and the reconnection to the session's cancel token: a cancelled or
replaced session stops reading, skips the reopen and exits without
raising an error.
* fix(dictation-pad): keep Test Dictation results and move pad routing to Settings
Keep late recognition callbacks on the test path.
Move Dictation Pad routing to Settings.
Fixes#983Fixes#986
* fix(recognition): ignore stop sentinels in pending transcription
Ignore stop signals when checking queued transcription work.
Add regression coverage for a real stop path leaving a sentinel behind.
Fixes#983
* fix(recognition): don't count orphaned queued audio as pending transcription
* fix(recognition): count superseded workers as pending transcription
* test(auto-pause): give the bare manager its worker-tracking state
* 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#979Fixes#980Fixes#981Fixes#982Fixes#985Fixes#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>
* feat(recognition): buffer audio during model reload
* fix(recognition): recover buffered reload failures
* test(recognition): cover buffered reload edge cases
* fix(recognition): narrow buffered reload errors
* fix(settings): wait for IDLE before restoring Test Dictation callbacks
Buffered reload's stop_recognition() returns while PROCESSING. Test
Dictation must keep its text callbacks until recognition reaches IDLE
so the test utterance cannot inject into the focused app.
* style: apply Black formatting to PR 851 files
* fix(settings): cancel buffered reload before Test Dictation timeout restore
On the ~3min idle-wait timeout, do not restore live injector callbacks while
a buffered reload worker may still deliver text. Cancel via
_cancel_reload_recording off the GTK thread, then restore; if cancel is
unavailable or fails, leave test callbacks installed and show an error.
Add return/param annotations and regression coverage.
* fix(settings): keep Test Dictation waiting if buffered cancel fails
After the idle-wait timeout, if cancel is unavailable or raises, keep
polling with test callbacks and _test_active instead of marking the test
done. Ending early left the test callback as the only text destination,
so later normal dictation could land in the hidden test buffer. Restore
live injectors only once recognition reaches IDLE.
* fix(settings): reset Test Dictation timeout-cancel flag per session
Clear _test_timeout_cancel_attempted when a test starts, when entering
the buffered idle-wait, and when restoring UI so a later Test Dictation
can still cancel after timeout. Narrow the cancel-worker except clause
to AttributeError/OSError/RuntimeError/TypeError so unrelated defects
are not treated as ordinary cancel failure.
* fix(recognition): signal capture completion when buffered capture dies
A dead microphone during a buffered model reload left should_record=false
with no stop_recognition handoff, so the reload worker blocked forever on
_capture_finished.wait(). Signal the event from the audio thread's failure
teardown paths instead.
Refs VocaHQ/vocalinux#851
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* 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.
* fix(settings): stop Test Dictation calling a missing model "no speech"
start_recognition now returns whether listening actually began. Test
Dictation checks that before starting its timer, so a missing or
auto-paused model gets a real error instead of "No speech detected".
* fix(settings): reconfigure Test Dictation against the live engine
UI matching config.json is not enough when speech_engine is still on
another engine or model size. Compare those too before the listen timer.
* fix(tests): keep settings_dialog lookups on one module
_load_settings_dialog() restored sys.modules but left
vocalinux.ui.settings_dialog pointing at the private reload.
Python 3.9/3.10 mock follows that attribute, so
test_whisper_list_and_delete patched a different copy than it
imported and the expanduser stub hid the real cache.
Point the package attribute back at the restored module, and
patch the imported module object in the deletion test.