Commit Graph
29 Commits
Author SHA1 Message Date
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Jatin K MalikDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
64032fb999 fix(snap): drop uinput plug denied by the Snap Store (#960)
* fix(snap): drop uinput plug denied by the Snap Store

The Snap Store rejected revisions 8-10 over the super-privileged uinput
plug (allow-installation), and reviewers denied the grant request on
forum.snapcraft.io/t/53528, pointing at the RemoteDesktop portal path
the app already prefers. Remove the plug and the uinput-only ydotool so
revisions pass automatic review again; Wayland typing continues through
the RemoteDesktop portal (already the default backend ahead of ydotool),
wtype, IBus Wayland on KDE, and clipboard paste.

KDE Plasma loses its correct-case ydotool path on the store snap (KWin's
portal mangles keysyms on some layouts, KDE bug 489021); the v0.18.x
GitHub .snap still declares uinput for KDE users who sideload it.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(text-injection): stop telling snap users to connect uinput

The Store snap no longer declares the plug, so the warning pointed at a
command that cannot work. The Snap hints now explain that only the
v0.18.x GitHub sideload declares uinput, and that native Wayland typing
uses the RemoteDesktop portal or IBus Wayland on KDE.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Jatin K Malik <jk.malik@yahoo.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 15:16:10 -07:00
d11bd1bf0d feat(post-processing): pipe transcriptions through an optional postprocessing script (#479)
* feat(post-processing): pipe transcriptions through user-defined script

Add a configurable post-processing step that passes each transcription
result to an external executable via stdin and uses its stdout as the
text to type. Enables custom transforms (formatting, LLM rewriting, etc.)
without modifying Vocalinux. Falls back to original text on script
failure or timeout.

* test(post-processor): add unit tests and extract apply_post_processing

Move config-reading logic from main.py closure into apply_post_processing()
in post_processor.py so it can be tested independently. Add 11 unit tests
covering PostProcessor.process() and apply_post_processing().

* docs(post-processing): document post-processing script feature

* fix formatting of one file

* fix(post-processing): run the user script with a stripped host environment

Rebase compat: the host_process AST guard requires every subprocess
spawned for a host binary to pass env=host_env() so an AppImage's
LD_LIBRARY_PATH/GI_TYPELIB_PATH/PYTHONHOME do not leak in. Also give
apply_post_processing() a typed ConfigManager parameter and keep
text_callback_wrapper's local variable typed as str for mypy.

* fix(post-processing): preserve paragraph breaks and tighten script handling

Take script stdout verbatim instead of rstripping every trailing
newline, so the "new paragraph" voice command's break survives a
passthrough script. A single trailing newline is still dropped, but
only when the transcription had none — it is almost always just the
line terminator echo/print-style tools append.

Also stop writing transformed output to the log (it can carry
sensitive dictation into retained logs), catch only the expected
process/OS/encoding failures instead of every Exception, and add the
missing signature annotations.

* fix(main): run the post-processing script off the recognition thread

A user script may take seconds per segment, and text callbacks run on
the recognition thread that drains the bounded audio-segment queue;
stalling it drops queued dictation once the queue fills. Submit
post-processing and injection to a dedicated single-worker executor so
the backlog waits instead of losing speech, while keeping injection
order. On timeout or failure the worker falls back to the original
text, and an unexpected error is logged with a traceback and still
injects the original rather than silently dropping the segment.

Also pin the callback tests' get_str mock to no-script so they
exercise the synchronous path deterministically, and add a callback
test proving transformed text (including preserved paragraph breaks)
reaches the injector.

* docs(post-processing): annotate new callbacks and clarify stdout contract

Add the missing type annotations on the new Settings page callbacks
per the repository guide, and document that script stdout is used
verbatim (a lone trailing newline added by line-oriented tools is
dropped only when the transcription had none) and that scripts run on
a dedicated worker.

* test(main): annotate new post-processing callback tests

* fix(main): bind queued dictation to its target app and drain worker on quit

Route every transcription segment through the single post-processing
worker (a pass-through when no script is configured) so clearing the
script path mid-queue can no longer let a later segment overtake an
earlier one still running.

A segment that can sit in the backlog — a script is configured or a job
is still running — is bound to the application focused when it was
dictated; the worker drops it if focus has verifiably moved to another
application, so delayed dictation cannot land in the wrong app.

Wire the worker into the tray quit path via an on_quit hook: it clears
the acceptance flag, cancels pending jobs, and waits — bounded by the
script's own timeout — so nothing injects after the injector stops.

* fix(main): keep voice actions ordered and probe focus off the recognition thread

- Queue voice-command actions on the post-processing worker so an action
  can no longer overtake dictated text still waiting to be injected.
- Move the submit-time focus probe onto a dedicated executor; the
  recognition thread only submits the probe, and the job joins the result
  at inject time, so compositor tools can no longer stall consumption of
  the bounded audio-segment queue.
- Re-check the quit flag after the inject-time focus re-probe, then shut
  both workers down without waiting on a running script so quitting does
  not freeze on the script timeout.

* fix(main): capture submit-time focus on dedicated threads and serialize quit

- Replace the shared focus-probe executor with a fresh daemon thread per
  submission so a slow earlier probe cannot delay a later one past the
  focus change it is meant to capture; jobs read the result off a
  one-slot queue at inject time.
- Give queued voice actions the same submit-time focus binding as text
  segments so a queued shortcut is dropped instead of fired into a newly
  focused application.
- Hold injection_lock across the accept-check plus inject/action call so
  quit waits only for an in-flight injection instead of racing it.
- Queue the IDLE reset of last_injected_text behind pending worker jobs
  so a queued 'delete that' still sees the text it targets.

* fix(main): keep actions instant on a free worker and bound quit wait

- An action now starts a submit-time focus probe only when it actually
  queues behind other jobs; with a free worker there is no delay window
  to bind against, so the probe's compositor calls are skipped and the
  action stays immediate even when a script is configured.
- Probe and executor submission now happen inside pending_jobs_lock so
  queue order matches callback order — a job that saw an empty queue can
  no longer slip behind one that arrived while it was submitting.
- Every inject-path subprocess call (xdotool window/key/type, wtype and
  ydotool type) is bounded by a timeout, and the quit hook's
  injection_lock wait is capped, so quitting can no longer freeze on a
  stalled injector subprocess.

* fix(main): bind every action to submit-time focus without slowing it down

- Every voice action now starts a submit-time focus probe: submission
  never guarantees immediate execution, so skipping the binding left a
  focus-change window in which a shortcut could fire into another app.
  The worker waits on the probe for at most one second — milliseconds
  on a healthy desktop — so immediate actions stay immediate while a
  genuinely stalled probe falls back to the injector's own targeting.
- Quit waits for an in-flight injection without a cap again: every
  inject-path subprocess is individually bounded, so the wait always
  ends with the injection and a long transcription is never cut off.
- The wtype/ydotool typing timeout is now derived from the configured
  per-key delay (a generous multiple), so a legitimately slow type can
  never be cut mid-text; a timed-out type no longer falls through to
  the clipboard fallback, which would duplicate the partial fragment.

* fix(main): drop unverified actions and make quit-time injection abortable

- An action whose submit-time focus probe cannot answer inside its
  one-second bound is now dropped instead of running unverified: a
  stalled probe can no longer leave a shortcut pointing at whatever
  application happens to be focused.
- Quit asks the injector to abort in-flight injections, and injection
  paths honour it: the xdotool chunk loop stops between chunks, the
  wtype/ydotool type call runs as a polling Popen that can be killed,
  and paste/shortcut/backspace entry points exit early. The quit hook
  still waits for an in-flight injection, but the wait is bounded by at
  most one already-running subprocess call — never the injection's full
  length — and no subprocess is ever stopped mid-call by a timeout cap
  on the wait itself.
- Annotate the remaining untyped test helpers.

* fix(main): wait out slow focus probes; restore run() in wayland typing

- An action no longer races its submit-time focus probe on a one-second
  bound: the job waits for the probe's own answer, whose compositor
  calls already carry per-call timeouts and short-circuit absent tools.
  A slow but living probe delays the command instead of discarding it,
  so a valid "undo" can no longer be lost to timing alone, and focus
  verification still decides every drop.
- Wayland typing goes back to subprocess.run so tests keep intercepting
  the same entry point. Abortability is preserved by typing in
  200-character chunks with the shutdown check between calls — a
  whole-string call could not be interrupted, but no single chunk holds
  the injector for more than its own scaled budget.
- The abort flag gains a class-level default so injectors built without
  __init__ in tests still answer the checks; each real instance gets its
  own event in __init__.
- Update call-shape assertions for the typed-call timeout and finish
  annotating the test methods touched by the callback refactor.

* fix(inject): never retype text a failed chunked call already sent

- _PartiallyTyped carries the confirmed-delivered prefix length out of
  the chunked type loops, so every downstream fallback continues from
  the untyped remainder: the Wayland-to-xdotool switch types only the
  tail, and the clipboard fallback copies only the tail, never the
  whole string a second time.
- The xdotool retry loop gains the same accounting — a retry now
  resumes at the first untyped chunk instead of restarting at the top
  and duplicating everything the failed attempt already delivered.
- Annotate the remaining test signature the repository guide requires.

* fix(inject): report partial typing as failure even when clipboard lands

A chunked type call that dies mid-text leaves only a prefix on screen,
and the remainder-fallback keeps it that way: it copies the tail for a
manual paste but now returns False, so the caller no longer records the
full transcription as injected and a following "delete that" cannot
erase text that was never typed.

* fix(inject): keep deletion state aligned with delivered text

inject_text now records how many characters a call confirmed on screen
as last_typed_count: the whole length on success, the typed prefix
after a partial failure, -1 when the delivered amount is unknowable.
The transcription caller uses it to keep last_injected_text honest —
a following "delete that" erases the typed prefix or nothing, never
the previous segment or text that never arrived.

* fix(inject): never replay input a timed-out portal job may have delivered

- RemoteDesktopPortalError.delivered=None marks an unreported job's count
  as unknowable; all three portal fallbacks demote and skip instead of
  replaying, and text injection surfaces as TimeoutExpired so no
  full-text clipboard copy is offered
- _PartiallyTyped counts raised after a portal demote are rebased onto
  the original text (typed_prefix) so the caller clips the real prefix
- evdev: a consumed pure-modifier press is withheld and replayed ahead
  of the first non-target press, so holding RightAlt for AltGr
  composition still reaches the application
- history: extend_latest gains an expected_latest CAS guard so a late
  segment from an older worker merges only into its own session's
  snippet, never into a newer session's entry
- quit path aborts in-flight injections before waiting on the lock so
  quitting cannot freeze behind a long transcription's typing budget
- tests: stop _ensure_ydotoold leaking a real Popen in Wayland tests

* fix(shortcut): replay withheld modifiers across devices and after SYN_DROPPED

- Replay withheld modifier presses from every device, not only the fd the
  character arrived on: split keyboards deliver AltGr and the character on
  separate devices, and a per-fd check forwarded a plain character.
- Let the post-SYN_DROPPED resync run when only withheld modifiers exist
  for the fd, so a press whose release the dropped burst ate is pruned
  instead of replaying a stale modifier on the next ordinary key.

* fix(history,pads): keep late segments pad-bound deliveries and pad deletions correct

- Track the worker behind the ended session's snippet so late segments
  from an older worker cannot merge into a newer session's entry
- Skip the focus-change drop for dictation-pad segments and pad-bound
  editing commands, which never target the focused application
- Refuse 'delete that' when a manual pad edit blurred the segment
  boundaries instead of deleting the stale recorded length

* style: drop unneeded nonlocal for mutated worker set

* fix(shortcut): share one evdev device layer across shortcut listeners

Each KeyboardShortcutManager built its own EvdevKeyboardBackend, and on
evdev every backend opened and grabbed the same physical keyboards: the
first grabber received all events and the rest received none, so
per-language shortcuts could never fire on Wayland.

All backends now bind to a process-wide EvdevDeviceHub that opens each
keyboard once, owns the grabs and the paired uinput clones, and fans
every event out to each registered engine. An event is re-emitted to
applications only when no engine consumed it, so suppression semantics
stay the same while per-engine matching state (combo progress, withheld
modifiers, swallowed presses) remains independent.

* fix(history): own late segments per worker and decide pad routing once

- ended_worker_snippets maps each ended session's worker to its own
  (snippet, commit epoch): late deliveries merge into their session's
  entry however many sessions have committed since, so one session's
  stragglers never split across Recent Snippets entries.
- The leftover-segment commit reads the live history epoch inside the
  session lock: a mid-session clear() no longer makes the stale ended
  epoch refuse valid post-clear dictation.
- The pad-bound routing decision is captured once per job and passed to
  the injector/action handler, so a toggle flip between the focus check
  and delivery cannot reroute text or shortcuts into the focused app.
- Dead recognition workers are pruned from the tracking set and map so
  long runs don't accumulate finished threads.
- The custom-terms switch now suppresses GTK's own state-set on save
  failure so it shows the value that actually persisted.

* fix(shortcut): serialize evdev device layer teardown against restart

The last listener's unregister released the engine lock before joining
the monitor thread and closing devices, so a new listener registering in
that gap opened keyboards the stale teardown then closed — it stayed
active but dead, and briefly grabbed keyboards stopped forwarding input.

A lifecycle lock now covers the whole close/open transition in register,
unregister, and the monitor's unexpected-exit teardown, so a cold start
can only open devices after the previous generation finished closing
(also making the new generation's grabs succeed). A generation stamp
tells a monitor thread that outlived its join() its incarnation is over
even when running has gone True again.

* fix(injection): log focus-probe failures and narrow clipboard fallback catch

A crashed focus probe turned into an unknown result with no logged
error, indistinguishable from unavailable focus information; the
partial-typing clipboard fallback caught every exception. Log the probe
failure with context and narrow the clipboard catch to the exceptions
the fallback path can actually raise.

* fix(shortcut): join the evdev monitor outside the lifecycle lock

If the monitor exited unexpectedly while the last listener unregistered,
its cleanup queued on the lifecycle lock that unregister() held for the
join — stalling shutdown for the join's full 2s timeout. The close now
finishes inside the lock and the join waits after it is released, so the
monitor's own cleanup can run and exit instead of queueing behind it.

Closing before the join also dies the monitor's read/select on the
closed fds right away. A _closed gate on _open_keyboard_device keeps a
rescan from opening a device between detach and close that nothing
would ever close.

* fix(injection): narrow focus-probe catch to the raisable exception set

get_focused_window() reports Optional and never raises; the catch now
covers only what can realistically escape the queue write and logs it
with context, keeping the waiting job released.

* fix(shortcut): tag evdev device fds with the generation that opened them

A monitor that outlived join() could still touch a newer generation's
devices: the loop re-checked the generation only at the top, so mid-iteration
it could dispatch from, forward to, or remove devices a restarted listener
had just opened.

Every fd is now tagged with the generation that opened it, and the tag is
verified at each point a monitor could mutate shared state — fd lookup for
dispatch, clone forwarding, forwarder release, device removal, and rescan
opens — while dispatch re-checks the generation between events and the
per-fd loop breaks on it. Cold starts bump the generation before opening
so fresh devices are tagged for the new generation. Devices an older
generation opened stay tagged stale, so its reader leaves them alone.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-30 03:55:17 -07:00
dca937fc21 feat(dictionary): combined custom dictionary with corrections (#890)
* feat(dictionary): combined custom dictionary with corrections

Rebased onto latest main (conflict-resolution squash for PR #818).

* fix(dictionary): stabilize the terms and corrections file contracts

- corrections_path() stays at the fixed config_dir() location promised by the file contract instead of following a custom or --dictionary-file terms path, which could strand or duplicate corrections; a leftover file beside the terms file is still read once and copied over.

- _atomic_write() preserves the replaced file's permission mode instead of resetting it to the process umask.

- build_initial_prompt() streams terms and stops at the configured cap instead of reading and processing the whole file for every segment.

* fix(settings): restore the selected dictionary pane after a search

Searching can auto-switch the Custom Dictionary management pane to reveal a match, but clearing the query only restored widget visibility. Record the pane selection in the search baseline snapshot and restore it alongside the previous page so users return to the pane they chose.

* fix(dictionary): close the remaining custom dictionary file-contract gaps

- A --dictionary-file session override no longer imports a corrections
  file found beside the override into the standard config directory;
  the override scopes to terms only.
- Terms reads validate the whole file before yielding: corrupt UTF-8
  mid-file now yields no terms at all, matching the invalid status
  Settings reports instead of leaking a partial dictionary to
  recognition.
- The terms read is bounded by MAX_TERMS_FILE_BYTES (1 MiB); a larger
  file is invalid everywhere with a dedicated status instead of being
  read on every segment.

* fix(dictionary): keep usable terms prefix on oversized files and legacy corrections under overrides

* fix(dictionary): cap yielded terms and keep added terms visible on oversized files

* fix(dictionary): keep added terms visible without dropping scanner-owned lines

* fix(dictionary): keep oversized terms manageable and honor mid-search pane switches

- terms list gains a Show all / Show first 500 toggle; adding a term
  beyond the cap auto-expands so the new row renders with a Remove
  button instead of being invisible until the file is hand-edited
- a manual dictionary pane switch during an active search drops the
  saved pre-search pane so clearing the query no longer rolls back the
  user's newer selection
- annotate the new settings tests and type model_info so the parakeet
  branch stops adding fresh mypy errors

* fix(settings): do not auto-expand terms on add past display cap

Rebuilding up to thousands of term rows on the UI thread stalls the
dialog; the overflow button already shows the true count, so the added
term stays one click away.

* test(dictionary): stub segment_callbacks on bare manager for history dispatch

* fix(dictionary): stream oversized terms files when building the prompt

build_initial_prompt decoded the whole 1 MiB read window before every
transcription segment even though it only needs the first max_words
terms. _iter_terms_for_prompt now reads oversized files line by line and
stops at the prompt limit, keeping the same contract: complete leading
lines only, invalid UTF-8 inside the usable window yields nothing.

* fix(history): only the ended session's worker may extend its snippet

A late segment could extend the newest history entry regardless of which
session produced it, so a worker finishing after the next dictation
session leaked its text into that session's entry. Track the worker that
owned the ended session and require late segments to arrive on the same
thread before merging; anything else forms its own snippet.

* fix(speech): transcribe queued segments the reload worker never drained

The recognition worker can exit during stop_recognition's bounded
audio-thread join, so the released key's final buffer lands in the queue
after the worker is gone and was silently dropped when the reload
teardown replaced the queue. Drain whatever remains before swapping it
— except on capture failure or an explicit cancel, which still discard
the session's leftovers.

* fix(settings): build term rows in idle slices and pin hidden additions

Two problems shared one cause. Rebuilding up to 4096 rows synchronously
stalled the dialog, and a term appended past the 500-row cap reported
success while staying invisible. Rows now build in bounded GLib idle
slices guarded by a refresh token, and a saved term outside the visible
window is pinned to the top of the capped list until it disappears or
becomes visible on its own. Adding under "show all" scrolls to the new
term instead.

* test(engine): annotate new test method return types

* fix(ci): match a candidate row that leads the latest track block

snapcraft status normally prints candidate as a continuation line under
the latest track (channel in column 1). If candidate ever leads the
track block it rides the track line instead (channel in column 3), and
the resolver missed it and refused to promote. Match both layouts in
the resolve and the pre-release re-verify.

* fix(dictionary): bound prompt reads and scroll to landed terms

- _iter_terms_for_prompt now reads each line via readline() capped by the
  remaining byte window, so an overlong line can never pull more than
  MAX_TERMS_FILE_BYTES into memory
- the prompt path keeps decoding every complete line inside the usable
  window after the term limit is collected, so invalid UTF-8 anywhere in
  the window still yields no terms and matches the reader's invalid status
- under "show all" Settings scrolls to the index where an added term
  landed (prepended or appended), verified against the row's label,
  instead of always scrolling to the end

* fix(history,reload): keep late-segment ownership and drain stamped tuples

- Track session_workers_seen so a worker from an earlier session can
  never merge into a newer snippet while a session is open
- Unpack (segment, language) queue tuples in _transcribe_queued_segments;
  passing the tuple as audio raised TypeError and lost final speech
- Refuse 'delete that' when a manual pad edit blurred segment boundaries
- Translate term-row coordinates into the scrolled card's space so the
  scroll actually reaches rows below the fold
- Type _on_dictionary_pane_changed's varargs

* style: drop unneeded nonlocal for mutated worker set

---------

Co-authored-by: Graham <graham@thegeeky.ninja>
2026-09-30 02:33:49 -07:00
8ebc4ce2ba feat(diarization): per-speaker transcripts via TinyDiarize + --transcribe-file (#884)
* feat(transcribe): add TinyDiarize file transcription surface

Catalog small.en-tdrz (hosted at akashmjn/tinydiarize-whisper.cpp,
pinned commit) while keeping it out of every dictation-model selection
path: its [SPEAKER_TURN] markup would type noise into the focused
window. The checksum generator resolves side-repo models at their own
pinned revision, parakeet-style.

Add speech_recognition.diarization (strict-WAV fast path with ffmpeg
fallback, tdrz transcription, speaker-turn parsing, timestamped
formatting) and surface it through a tray Transcribe Audio File flow
with on-demand model download plus a headless --transcribe-file CLI.
TranscriptDialog offers copy and save of the diarized transcript.

Closes #756

* docs(user-guide): document --transcribe-file

* fix(transcribe): pass tdrz_enable as a pywhispercpp kwarg

pywhispercpp's Model takes decode params as keyword arguments; a
params= dict crashes on the C-level whisper_full_params object. Also
let --transcribe-file report unexpected failures cleanly instead of
dumping a traceback.

* fix(tray): dismiss the tdrz download dialog on response

* test(diarization): pin real wave/numpy/tempfile around sibling mocks

Several test modules replace sys.modules entries with MagicMocks at
import time and never restore them, so diarization's wave/numpy/
tempfile bindings resolved to mocks when this file ran after them in
CI. An autouse fixture reimports the real stdlib modules per test, the
same remedy test_audio_feedback already applies for wave.

* fix(transcribe): verify tdrz model, preload libs, gate formats on ffmpeg

- Hash the on-disk TinyDiarize model against the pinned digest before
  native code maps it, matching the dictation load path
- Extract the bundled-lib preloader to utils/pywhispercpp_loader.py and
  preload before the lazy pywhispercpp import so the headless
  --transcribe-file path works on source-built installs without RPATH
- Offer only WAV in the file picker when ffmpeg is absent; name the
  dependency in the filter label
- Type the new tray/diarization callbacks; document --transcribe-file
  and the new modules in AGENTS.md

* style(audio): finish type annotations on capture helpers and download path

_setup_alsa_error_handler now declares Optional[Any] and its ctypes
callback gets concrete parameter types; the remaining capture helpers
take audio: Any (PyAudio has no importable type) and test_audio_input's
device_index becomes Optional[int], which also clears the preexisting
implicit-Optional error. _download_whispercpp_model gains -> None and
its HF url is narrowed to str so the concatenation and download call
stop adding new mypy findings.

* style(tests): wrap long monkeypatch line in preload test

* refactor(main): narrow transcribe-file errors and add file context

* style(ui): annotate TranscriptDialog constructor

* fix(diarize): log worker tracebacks, isort test helpers, document --transcribe-file

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-30 00:44:55 -07:00
f36c3f6ae5 feat(text_injection): add RemoteDesktop portal injection backend (#885)
* feat(text_injection): add RemoteDesktop portal injection backend

Wayland offers exactly one sanctioned key-injection path:
org.freedesktop.portal.RemoteDesktop's NotifyKeyboardKeysym. It reaches
native clients on compositors with no input-method-v2 bridge, works from
inside the Flatpak sandbox without /dev/uinput or a helper daemon, and
delivers keysyms so non-US layouts survive -- unlike ydotool's evdev
keycodes.

RemoteDesktopPortal is a synchronous client over a dedicated worker
thread holding a private bus connection and GLib context, which keeps
Request::Response delivery deterministic. Sessions are created lazily on
first use so startup probing only reads the interface version; v2 portal
sessions persist a restore token (mode 0600 under data_dir) so the
permission prompt appears once.

Backend selection now prefers the portal on Wayland: it is probed at
runtime ahead of ydotool/wtype/xdotool in autodetection, is pinnable via
VOCALINUX_FORCE_BACKEND=portal or text_injection.backend, and demotes to
the previous order on runtime failure. Flatpak still needs --device=all
for evdev hotkeys, but uinput is only a fallback path now.

Closes #750

* fix(text_injection): resolve Greptile findings on RemoteDesktop portal injection

- Move persist_mode/restore_token to SelectDevices options (portal v2
  sends them there, not CreateSession); a bad token is ignored rather
  than sinking the session, so drop the stale-token retry paths that
  could issue a second Start after a user cancel
- Track delivered units on RemoteDesktopPortalError so Wayland
  fallbacks retry only the undelivered tail instead of retyping the
  prefix (text chars, backspace taps, shortcut steps)
- Give queued portal jobs a single deadline budget across
  CreateSession/SelectDevices/Start waits; on submit timeout the job
  is cancelled via a Gio.Cancellable plus abort checkpoints so the
  worker cannot keep emitting keys after the caller gave up
- Close sessions whose Start never succeeded
- Release held shortcut modifiers on mid-step failure
- Skip the portal probe when an installed wtype/ydotool backend is
  explicitly pinned
- Type annotations on the new signatures and touched tests

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 23:55:27 -07:00
a89966a744 feat(recognition): record while an idle model reloads (#851)
* 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>
2026-09-29 22:02:56 -07:00
Web-BenefitsGitHubAlexanderDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
db07590761 feat(activation): D-Bus activation for compositor global shortcuts (#568)
Rebased onto latest main (conflict-resolution squash for PR #568).

Co-authored-by: Alexander <ab@digital-benefits.de>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-29 21:19:29 -07:00
Jatin K MalikandGitHub eef2554f7c feat(audio): lower other audio while dictating (#861)
* feat(audio): lower other audio while dictating

Opt-in setting lowers the default PipeWire or PulseAudio sink while the
microphone is open, then restores that exact level. It stays off by default.

* fix(test): keep pytest.approx working when numpy is mocked

Other test modules replace sys.modules["numpy"] with a MagicMock at import
and leave it there. pytest.approx then crashes on isinstance(..., numpy.bool_).

* fix(audio): restore every channel and do not duck if it cannot be undone

A failed restore-point write no longer lowers the sink. PulseAudio puts
each channel back, and a dead microphone restores playback instead of
waiting for an explicit stop. The recognition test patches the real
audio module when an older test left a mock behind.

* fix(test): put the audio-feedback mock back after the ducking test

The recognition test has to patch the real module, but later tests still
expect the mock that test_text_injector installs. Restore that mock when
the test ends, and annotate the new helpers.

* fix(test): annotate the playback-duck tests

AGENTS.md requires a type hint on every new function. These tests were the
ones still missing them.

* fix(audio): skip arming playback duck after recording already failed

* chore: retrigger review after rebase
2026-09-29 14:24:28 -07:00
HashingandGitHub 12e440ce1c feat(text-injection): let config.json pin the injection backend (#649)
* feat(text-injection): let config.json pin the injection backend (#476)

Autodetection has to infer whether IBus commits actually reach the
focused application, and it cannot verify that: commit_text() reports
success even when the compositor drops the text. When the inference is
wrong the failure is silent, and dictation appears to work in XWayland
windows while doing nothing in native ones.

VOCALINUX_FORCE_BACKEND already provides an escape hatch, but it is an
environment variable: it covers a single run, is undocumented outside
the test suite, and a user whose compositor is misdetected has to wrap
the launcher in a shell script to make the choice stick.

Add a persistent `text_injection.backend` setting alongside it, accepting
the same ibus/wtype/ydotool/auto values. The environment variable still
wins so a backend can be A/B-tested for one run without touching the
saved configuration, and the startup log now names which of the two
pinned the choice so a stale setting is diagnosable from --debug output.

Reads the file directly rather than through ConfigManager, keeping this
package independent of the UI layer, exactly as _should_copy_to_clipboard
already does. An unreadable or corrupt config falls back to autodetection
instead of blocking startup, and an unrecognised value is ignored with a
warning so a typo cannot silently pin the wrong backend.

Defaults to "auto", so existing installs behave exactly as before.

* fix(text-injection): tell an explicit VOCALINUX_FORCE_BACKEND=auto from unset

The variable is meant to override the saved text_injection.backend pin for a
single run, which is what makes the two paths A/B-testable without editing
config. Setting it to auto -- the value that means "autodetect this time" --
did not do that: the reader collapsed unset and an explicit auto into the same
"auto" return, so the caller read both as "nothing was forced" and fell through
to the saved pin. The pin won, and there was no way to ask for autodetection
for one run short of editing the config file the variable exists to avoid.

Read the variable as three states instead: unset, an explicit auto, or a
backend name. Only the unset state consults config, so an explicit auto now
short-circuits to autodetection.

An unrecognised value is deliberately treated as unset rather than as auto. A
typo means the user failed to override their preference, not that they asked to
discard it, so falling through to a valid saved pin is the kinder reading.

_forced_backend() keeps its signature and its collapsed return, so its existing
callers and tests are untouched; the new three-state reading lives alongside it
and is what the preference resolver consumes.

* feat(text-injection): accept an xdotool pin and warn when ibus overrides the denylist

Two gaps in the pinnable backend set.

xdotool was not accepted, even though on X11 it is the tool that actually does
the typing. Users were told to pin a *Wayland* backend to mean "not IBus", and
get xdotool as a side effect. Naming it directly is what they were reaching for.
On Wayland it needs no special handling: the value skips IBus like any other
non-ibus pin and falls through to the existing XWayland fallback, so no branch
is added to the selection chain.

The accepted values now live in one tuple, because four places listed them --
both readers and both "expected ..." messages -- and a fifth value would have
had to be added to all four. The two messages derive from the tuple, so they
cannot fall behind it.

Pinning ibus also silently bypasses the compositor denylist. That check exists
because a denylisted compositor makes IBus report a commit as delivered while
the text never arrives (#478, #485), so a pin is the one setting that can put a
user back into a failure with no symptom other than nothing happening. The pin
is still honoured -- an override that ignores you is worse than one that warns
-- but it now says so at warning level, names the compositor, and gives the
remedy. It stays quiet when the compositor genuinely bridges IBus, including a
denylisted one running the ibus-wayland bridge, and on X11 where the denylist
does not apply.

The check is evaluated once. The guard chain short-circuits on the pin, so a
pinned run never reaches the existing compositor branch, and this is its first
and only call -- worth knowing because the check can shell out to pgrep or
gdbus. A test asserts the call count so that stays true.

Documents xdotool in the value table and rewrites the X11 paragraph, which
described xdotool as the implicit result of pinning something else rather than
as a value you can name.

* fix(text-injection): say so when the pinned backend is not the one in use

A pin could be quietly ignored. Pinning a tool that is not installed fell
through to autodetection and typed with whatever it found, under a log line
that had already announced the pin was overriding autodetection. Pinning
xdotool on Wayland did nothing at all, since the selection chain reaches
ydotool and wtype first. Pinning ibus where IBus support is unavailable skipped
the whole IBus branch. In each case the user set a preference, saw no
complaint, and got something else.

These are one condition, not three: whatever ended up doing the typing is not
what was pinned. The check compares the pin against the backend read back from
final state, so a cause nobody has thought of yet is covered without another
branch. It runs at the end of construction rather than inside
_check_dependencies, which returns early on two paths and which the wtype probe
can still override afterwards. The reason clause is added only where a generic
check answers it -- the binary is absent, or IBus is unavailable -- and omitted
otherwise rather than guessing.

Reporting the source separately was letting the environment variable and the
config setting disagree. _check_dependencies resolved the backend, then
re-derived which source had won by asking again -- so the environment was
parsed twice per construction, and a malformed value was reported to the user
twice. Resolution now returns the backend and its source together, which fixes
the duplicate warning and means the source named in a log line is the one that
actually chose the backend. _backend_preference() keeps its signature and reads
the first element.

Two log lines also claimed VOCALINUX_FORCE_BACKEND regardless of where the pin
came from, so a config-set backend was reported as an environment variable the
user had never set. They now name the real source.

The new warning is a startup diagnostic, not a live invariant: a tool switched
later by _try_recover_from_fallback is not reported again. Noted in the
docstring so the boundary is deliberate.

* docs(user-guide): file the injection-backend section under Troubleshooting

The section sat under Customization, directly beneath GPU Acceleration --
where the paragraph immediately above it reads "To check which backend is
being used" about Vulkan and CUDA. The word means something else there, and
two adjacent sections using it for different things is a bad place to explain
either.

It also is not a customization option. It exists because a compositor can make
IBus report a commit as delivered while nothing arrives (#478, #485), and
someone hitting that reads Troubleshooting, not Customization. The section
already opens on the symptom, so it needs no rewriting to belong there.

Pure reorder: no wording changes, and the heading text is unchanged so any
existing link still resolves.

* test(text-injection): cover the pin paths that skip the selection chain

_check_dependencies had three uncovered branches, all reachable by pinning a
backend and none of them exercised: the two early returns that hand over to
IBus before a tool is chosen, and the daemonless-ydotool fallback a wtype pin
lands on when wtype is absent. The early returns matter most, since the pin
check runs at the end of construction precisely so those paths cannot skip
it, and nothing proved that.

Adds the ydotool counterpart to the missing-binary case, so a check keyed on
one tool name cannot pass by coincidence; an X11 test for the guide's claim
that any non-ibus pin turns the IBus path off, with a control asserting IBus
is constructed without a pin so the assertion is the pin's doing; and an
end-to-end explicit-auto case, which until now was only proven at the
resolver rather than through selection.

The pin log line said it was "overriding backend autodetection" before
anything had been checked. An unavailable pin falls through to autodetection,
so that line announced an outcome it could not know and then read as
contradicting the "was not applied" warning. It now states the request only.

_run_with_pin gains ydotoold_ready, defaulting to the previous behaviour so
existing callers are unchanged.

* fix(text-injection): report a config file the backend pin cannot be read from

A corrupt config.json disappeared into logger.debug. The setting is
hand-edited -- that is the workflow the user guide documents -- so a stray
comma is a likely way to reach this, and staying quiet left the user with
autodetection and the silent IBus miss the pin was set to avoid, with nothing
to tell that apart from the pin not working. Unknown values already warned;
an unusable file now does too, naming the file and the parse error.

The bare handler was also catching AttributeError by accident. Valid JSON
that is not an object -- a bare list, string, number or null -- reaches the
lookup and raises there rather than at parse time, and so does a
text_injection section that is not an object, where the top level is a
mapping and the section is not. Both are now shape checks rather than
exception handling, which is what lets the handler narrow to OSError and
ValueError without those five shapes escaping it.

Locating the file moves out of the try. It fixes the handler being unable to
name the path it failed on, and it is a deliberate boundary: config_dir()
resolving a path is not a failure this should paper over, so it is no longer
guarded here at all.

A missing config.json stays silent, which is the default install.

* test(text-injection): stop the ibus-pin tests depending on an installed ydotoold

The bridged-compositor and X11 cases asserted that a run logs no warnings at
all. That is broader than what they are about, and _run_ibus_pin patches
shutil.which to claim every tool exists without patching the probe that then
shells out to one, so the real _ensure_ydotoold ran and warned whenever
/usr/bin/ydotoold was absent. The tests passed on a machine that happens to
have it installed and failed on the CI runners that do not.

The assertions now name the warning they are about, and the helper patches
_ensure_ydotoold the way its sibling _run_with_pin already did, so nothing in
this class of test reads the host's installed packages.

* fix(text-injection): report pending IBus initialization state

* docs(text-injection): clarify backend pin behavior

* fix(config): preserve a hand-edited backend across unrelated saves

ConfigManager writes its full in-memory configuration on every save. A user who edits text_injection.backend in config.json while Vocalinux is running therefore loses that choice when an unrelated Settings or tray save writes the stale default back.

Remember only the backend value observed at load or after a successful save. If the in-memory value is unchanged, preserve the current value from disk; if it changed through ConfigManager, treat that as the intentional write. Missing and explicit auto both reset to auto.

The save aborts rather than overwriting a file whose JSON or text_injection shape cannot be read safely. This is deliberately per-key: no recursive configuration merge or injection-layer dependency is added.

* style(text-injection): remove redundant typing suppression

* fix(config): keep invalid UTF-8 config from blocking startup

* fix(config): preserve malformed backend sections on unrelated saves

* fix(config): retain text setting changes after malformed config

* test(text-injection): annotate backend pin test helpers
2026-09-29 12:24:59 -07:00
7bdbedce0b feat(language): follow the active keyboard layout while dictating (#837)
* feat(language): follow the active keyboard layout while dictating

Adds a "Follow keyboard layout" switch to Settings -> Speech Model ->
Advanced. With it on, the active keyboard layout is re-read at the start
of every dictation and the matching language is used, so someone who
switches layouts to work in two languages gets the right one without
opening Settings.

#796 already seeded the language once on first run, but from the
*configured* primary layout, which never changes as the user switches
between them: a "fr,us" setup reports fr forever. detect_active_keyboard_
layout() reads GNOME's input sources instead. It takes mru-sources rather
than the current index, because that index goes stale -- observed still
reading 0 after a switch to the second layout -- which is why the IBus
injection path already trusts the MRU list (#497, #738).

An active source that is an IBus engine rather than an xkb layout is a
real answer that cannot be mapped, so the language is left to the engine's
own auto-detect. Borrowing the configured xkb layout there would pin
English for someone typing Japanese through mozc-jp.

The mode is a switch over the language picker rather than an entry inside
it, since it is a mode and not a language. While on, the picker is
disabled and shows what the layout resolves to; turning it off pins that.

A language change no longer re-initialises the engine when the loaded
model takes the language per utterance, which is the whole whisper
family, so following a layout costs a settings read rather than a model
reload. VOSK loads one model per language, Parakeet ignores the picker,
and an English-only model has no non-English weights, so Settings refuses
the mode for all three rather than offering a switch that does nothing.

Fixes #821

* fix(language): keep follow mode armed and map IBus engines

Swap English-only whisper.cpp weights to the multilingual sibling while
passing the layout sentinel into reconfigure, so follow mode stays armed.
Map common GNOME IBus engines onto catalogue languages instead of
leaving them unmapped.

* fix(language): stop IBus hyphen false-maps and gate the sibling swap

The last-token hyphen heuristic mapped mozc-us and foo-us to English.
Restrict it to *-jp/*-kr and name Mozc's extra engines as Japanese.
Only whisper.cpp may consult the whisper.cpp download catalog.

* fix(language): keep multilingual weights while following layout

The picker shows the active layout while follow is on, often en-us.
Deriving whisper.cpp .en from that display made Settings refuse the
mode as English-only, then the next auto-apply persisted a concrete
language and disarmed follow. Use LANGUAGE_FOLLOWS_LAYOUT for .en vs
multilingual derivation instead, including on the load path before
the switch is synced.

* fix(language): type follow helpers and log sibling-swap failures

* fix(test): stub follow-layout model rec so mocked psutil does not blow up

follow-mode English-only guard calls get_recommended_whispercpp_model, which
reads RAM through psutil. Full CI leaves a MagicMock in sys.modules["psutil"],
so ram_gb >= 16 raises TypeError. Patch the settings_dialog alias to return a
multilingual tiny rec so the assertion still holds without real RAM.

* fix(language): catch specific errors on layout lookup and sibling swap

* fix(language): swallow more sibling-swap reconfigure failures

ImportError, TypeError, AttributeError, and ChecksumError from the
multilingual swap must not abort dictation or disarm follow mode.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 01:06:32 -07:00
Jatin K MalikandGitHub f48bdda525 docs: improve project documentation for a clearer public surface (#634)
* docs: polish project documentation for a clearer public surface

Rewrite the README and supporting docs for a more professional first
impression: less emoji and badge noise, install-first structure, and a
real documentation index. Add a Contributor Covenant, tighten SECURITY
and GitHub templates, and make distro/update guides read as product docs
instead of internal status notes.

* docs: split install guide and add changelog plus support pages

Break INSTALL.md into a short primary guide, manual/PyPI package lists,
and troubleshooting. Add root CHANGELOG.md and SUPPORT.md, wire them
into the docs index and README, and drop the outdated screenshot lag note
now that v0.15 assets are on main.

* docs: humanize PR copy and remove em dashes

Strip em/en dashes from the documentation rewritten in this PR, fix
mid-sentence colon fallout from that pass, and smooth a few stiff
phrases so the public docs read more natural.

* docs: list Faster Whisper after rebase onto main

Installer and CLI already accept faster_whisper. Document it next to
the other engines, drop the outdated screenshot lag note, and keep the
on-device wording honest for local vs remote.
2026-09-07 22:51:14 -07:00
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>
2026-09-07 20:47:12 -07:00
Jatin K MalikandGitHub 2c6acbc15e fix(inject): layout-aware clipboard paste for Neo and friends (#788)
* fix(inject): resolve paste chord for non-QWERTY layouts

ydotool was sending physical KEY_V (47) for Ctrl+V. On German Neo that
key types p, so paste opened Print instead. Prefer wtype keysyms when
usable; otherwise resolve the evdev key for Latin v via xkbcommon, and
read KDE kxkbrc so Plasma Wayland Neo users are covered.

Fixes #787

* fix(inject): read Plasma active layout from real session sources

kxkbrc has no CurrentLayout/LayoutIndex, so multi-layout Neo always
fell back to us at index 0 and paste still hit KEY_V. Prefer D-Bus
getLayout or layout_memory.xml for the active layout, keep kxkbrc
for the list only, and prefer KDE over leftover GNOME on Plasma.
2026-09-07 18:10:43 -07:00
Jatin K MalikandGitHub 008fd3138e fix(injection): use Ctrl+Shift+V when pasting into terminals (#734)
* fix(injection): use Ctrl+Shift+V when pasting into terminals

Clipboard injection always sent Ctrl+V, which most terminal emulators
ignore. Detect standalone terminal windows and send Ctrl+Shift+V, with a
Settings override for nested IDE panels that window-class detection cannot see.

* fix(injection): fail closed when terminal window probe is mocked

Clipboard tests patch text_injector.subprocess.run, which replaces
stdlib subprocess.run. Window detection now ignores non-string probe
output so injection still uses Ctrl+V instead of raising TypeError.

* docs: fix overclaimed terminal auto-detect in USER_GUIDE

Auto-detect only runs on X11 and on Hyprland, Sway, and niri. GNOME and
KDE Wayland stay on Ctrl+V unless you set Clipboard Paste Shortcut to
Ctrl+Shift+V.
2026-08-27 00:12:27 -07:00
Jatin K MalikandGitHub 279d99fea0 fix(whisper): honor bundled GPU libs and skip software Vulkan devices (#674)
* fix(whisper): use bundled GPU libs and skip software Vulkan devices

AppImage 0.15.0 shipped a CPU-only pywhispercpp wheel, so the Settings
GPU picker and host vulkaninfo never moved inference off the CPU. Rebuild
pywhispercpp with Vulkan in the AppImage, log the libraries that actually
load, skip llvmpipe in auto-select, and document how to verify.

Fixes #604

* fix(appimage): pin gcc/g++ for Vulkan pywhispercpp rebuild

Hosts that default cc/c++ to clang fail CMAKE's C++ test because clang
does not search gcc's libstdc++ path. Default the rebuild to gcc/g++.
2026-08-16 18:29:07 -07:00
Jatin K MalikandGitHub ecd5839a13 feat(settings): delete unused downloaded speech models (#671)
* feat(settings): delete unused downloaded speech models

Add a Remove Model dropdown on the Speech Model page so whisper.cpp,
Whisper, and VOSK downloads can be deleted from Settings. The currently
selected model is marked in use and cannot be removed until you switch
away from it.

Fixes #650

* fix(settings): move unused model deletion below the picker

Unused downloads is its own group under the model info card. Only leftover
files appear, each with a Delete button. The group hides when empty.

* fix(settings): collapse unused downloads into an expander

Keep leftover models behind one row so a long list does not stretch the
Speech Model page. Expand to delete files one at a time; the list scrolls
once it grows past a few entries.

* fix(settings): keep leftover model list from clipping in the expander

Wrap the unused-downloads list so the expander can show a few rows and
scroll the rest, instead of painting only the first leftover.
2026-08-12 23:42:36 -07:00
Jatin K MalikandGitHub c337d8f56d feat(shortcuts): default to hold Right Alt (push-to-talk) for new installs (#648)
* feat(shortcuts): default to hold Right Alt push-to-talk on first install

Double-tap Ctrl conflicts with many desktop and app shortcuts, so new
installs now use hold Right Alt (Option) in push-to-talk mode. Existing
config.json values are preserved unchanged.

Updates README, user guide, AGENTS.md, first-run dialog, website copy,
and tests to match the new defaults.

* refactor(shortcuts): trim redundant default-change test and comments

Drop the fresh-install assertion that duplicated DEFAULT_CONFIG coverage,
remove the duplicated Right-Alt rationale from config_manager, and drop a
vacuous source-string check from the settings shortcuts test.

* fix(shortcuts): align install.sh seeds and preserve legacy toggle mode

install.sh still wrote ctrl+ctrl without mode, so merges with the new
push_to_talk default became hold-Ctrl. Seed right_alt + push_to_talk for
new installs, and migrate existing shortcut-only configs to mode=toggle
so prior behavior is unchanged.

* style(config): fix black formatting after shortcuts migration

* fix(shortcuts): preserve legacy defaults when shortcuts section is missing

Existing config files without a shortcuts section previously inherited
ctrl+ctrl toggle from DEFAULT_CONFIG. Pin those historical defaults on
upgrade so the new first-install Right Alt push-to-talk defaults do not
silently change behavior.

* docs(agents): unwrap hard-broken dictation control paragraph

Keep the AGENTS.md paragraph as a single line like the surrounding prose.
2026-08-08 00:07:54 -07:00
67dde781f5 docs(user-guide): note CUDA device 0 limit for dual NVIDIA (#644)
Document that CUDA-backed pywhispercpp always uses device 0, and
point to CUDA_VISIBLE_DEVICES or a Vulkan build as workarounds.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-08-05 21:56:02 -07:00
fbb81238f5 chore(release): prepare v0.15.0 (#609)
* chore(release): prepare v0.15.0

Bump version across app, web, AUR, and Flatpak metainfo. Document GitHub
release-notes rules, refresh README/UPDATE/INSTALL/USER_GUIDE for the 0.15
series (sidebar settings, AppImage, auto-pause/keepalive, Vulkan device
selection), and align website product surfaces with shipped features.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* docs(release): fold latest main into v0.15.0 notes

Include dictation capitalization/trailing-space, ibus-wayland injection,
IBus teardown, and settings notice polish landed on main after the initial
release prep rebase.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* docs(release): include Bluetooth mic crash fix in v0.15.0 notes

Fold #599 (PortAudio Bluetooth SCO probe heap corruption) into README,
UPDATE, changelog, AppStream, and release history after rebasing onto main.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* docs(release): include language catalog expansion in v0.15.0 notes

Fold #616 (Hungarian and expanded speech languages, Fixes #565) into
README, UPDATE, changelog, AppStream, PRODUCT.md, and release history
after rebasing onto latest main.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* docs(release): fold settings footer and language follow-ups into v0.15.0

Include #618 sidebar dictation footer, #619 Custom Shortcut Record/Set
restore, and #617 English (India) Whisper language mapping after rebasing
onto latest main.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-08-02 15:26:03 -07:00
Jatin K MalikandGitHub e0ac5e409c feat(whisper): add guided whisper.cpp model variants (#465)
* feat(whisper): refine whisper.cpp model selection

* refactor(whisper): exclude diarization model from picker

* docs(whisper): document guided model variants

* fix(ui): cap settings dialog height at 600px to prevent excessive height on high-res displays
2026-06-11 22:22:43 -07:00
Jatin K MalikandGitHub 78f377fd0f fix(models): correct download size metadata for whisper.cpp and VOSK (#453)
* fix(models): correct download size metadata for whisper.cpp and VOSK

The size_mb values in WHISPERCPP_MODEL_INFO were using parameter counts instead of actual download sizes, causing the UI to display incorrect sizes (e.g., showing 39 MB when actual download is 74 MB). Updated all whisper.cpp models with verified sizes from Hugging Face and adjusted VOSK medium/large sizes to better represent actual downloads.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* docs: update model download sizes to match actual downloads

Update whisper.cpp size references from parameter counts to verified download sizes (e.g., tiny: 39MB → 74MB, medium: 769MB → 1.5GB, large: 1.5GB → 3.0GB).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

* docs(web): update model sizes across website pages

Sync whisper.cpp size references on gpu-acceleration, compare, whisper-model-guide, offline, and homepage with corrected download sizes.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
2026-06-06 16:54:21 -07:00
Jatin K MalikandGitHub 0469defae6 chore(release): prepare v0.8.0-beta (#280)
* chore(release): prepare v0.8.0-beta

* docs(web): align release messaging with v0.8.0-beta features

* docs(web): tighten homepage messaging for v0.8.0-beta
2026-02-28 20:58:55 -08:00
Jatin K MalikandGitHub 9886e8cb9d feat(ui): add auto-start on boot support (#207)
* feat(ui): add auto-start on boot support

- Add --start-minimized CLI flag for autostart entry
- Create autostart_manager.py to handle XDG autostart desktop entry
- Add autostart config option in config_manager.py
- Add Start on Login toggle in Settings dialog
- Add Start Minimized toggle in Settings dialog
- Add Start on Login checkbox in system tray menu
- Add first-run dialog to ask about autostart preference

Closes #184

* fix(test): add first_run: False to mock config settings

The tests were failing with StopIteration because main() now checks
for first_run and shows a dialog if True. By setting first_run: False
in the mock config, we skip the first-run dialog code path.

* fix(ui): harden autostart behavior across Linux sessions

Use XDG-compliant autostart paths and robust Exec command resolution, keep first-run prompt semantics correct, and fix GTK toggle handling so state changes are applied reliably.

* docs(autostart): document XDG startup model and limits

Clarify that Vocalinux uses desktop-session XDG autostart, not systemd services, and explain compatibility expectations for mainstream DEs versus minimal/custom WM sessions.

* feat(ui): add first-run quick-start guidance text

Expand the welcome dialog with concise first-time usage guidance and clear direction to the tray menu Settings entry while preserving the existing autostart decision flow.

* fix(uninstall): remove XDG autostart desktop entry

Ensure uninstall removes ~/.config autostart registration and reports leftover autostart entries during verification.

* fix(install): ignore defunct processes in running-check

Filter installer process detection to exclude defunct entries and installer/uninstaller shells so stale zombie rows do not block reinstall.

* fix(uninstall): ignore defunct processes in running-check

Use filtered process detection so zombie entries and installer shells do not trigger false running-process warnings during uninstall.
2026-02-21 15:06:20 -08:00
Jatin K MalikandGitHub 03d55a989f chore(release): prepare v0.6.0-beta with whisper.cpp as default (#181)
* docs: update all documentation for whisper.cpp as default engine

Comprehensive documentation update highlighting whisper.cpp as the new
default speech recognition engine:

README.md:
- Added 'Major Change: whisper.cpp is Now Default!' section
- Updated Features to highlight whisper.cpp and Vulkan GPU support
- Updated Quick Install section with engine choices
- Changed default engine from whisper to whisper_cpp in examples
- Updated configuration examples
- Updated roadmap to mark whisper.cpp as complete

docs/INSTALL.md:
- Added whisper.cpp section with performance benefits
- Updated installation options (--engine flag)
- Added GPU support matrix and Vulkan detection info
- Added installation time comparison table
- Updated system requirements (lower disk space for whisper.cpp)

docs/USER_GUIDE.md:
- Added comprehensive 'Choosing Your Engine' section
- Added GPU acceleration information
- Updated tips to mention whisper.cpp benefits
- Added model size recommendations

web/src/app/page.tsx:
- Updated hero to mention whisper.cpp (C++ optimized, Vulkan support)
- Changed installation time from ~5-10 min to ~1-2 min
- Updated Features section to mention Vulkan GPU support
- Added whisper.cpp as default engine card (3-column layout)
- Updated installation commands (--engine flags)
- Updated FAQ about switching engines

This ensures all documentation reflects whisper.cpp as the new default
and highlights its major benefits: 10x faster install, universal GPU
support, and C++ optimized performance.

* chore(release): prepare v0.6.0-beta

This release brings whisper.cpp as the new default speech recognition engine:

Version Updates:
- Bump version from 0.5.0-beta to 0.6.0-beta
- Update src/vocalinux/version.py
- Update web/package.json

Documentation Updates:
- README.md: Update all v0.5.0-beta references to v0.6.0-beta
- docs/INSTALL.md: Update version references and installation commands
- docs/UPDATE.md: Add new 'What's New' section highlighting whisper.cpp
- SECURITY.md: Update supported versions table (0.6.x now supported)

Website Updates:
- web/src/app/layout.tsx: Update softwareVersion to 0.6.0-beta

Release Highlights:
- whisper.cpp is now the default engine (10x faster install!)
- Universal GPU support via Vulkan (AMD, Intel, NVIDIA)
- C++ optimized inference with multi-threading
- ~1-2 minute installation vs ~5-10 minutes before

Testing:
- All 472 tests passing
- Black formatting clean
- Flake8 linting clean
- isort import ordering clean

* ui(website): improve hero section layout

* docs(readme): remove duplicate What's New section
2026-02-11 20:33:28 -08:00
Jatin K MalikandGitHub f8e5de5621 feat: Make Whisper tiny the default engine and download during install (#45)
* feat: Make Whisper tiny the default engine and download during install

- Changed default engine from 'vosk' to 'whisper' in config_manager.py
- Changed default model_size from 'small' to 'tiny' in config_manager.py
- Added install_whisper_model() function to install.sh that downloads
  the Whisper tiny model (~75MB) during installation
- Whisper model is downloaded when Whisper package is installed
  (in non-interactive mode or with --with-whisper flag)
- Updated installation summary to show Whisper model status
- Updated documentation (README.md, INSTALL.md, SECURITY.md, USER_GUIDE.md)
  to reflect Whisper tiny as the default engine

This provides a better out-of-box experience: users can install and
immediately use Vocalinux without any additional downloads or prompts.

* test: Update test to reflect whisper as default engine
2025-12-28 20:31:29 -08:00
Jatin K MalikandGitHub a33c0d1b4c Simplify Keyboard Shortcuts to Exclusive Double-tap Ctrl Implementation (#9)
* Implement double-tap Ctrl keyboard shortcut feature

* Simplify keyboard shortcuts to only use double-tap Ctrl

* Update docs

* Fix syntax

* Fix CI build by adding portaudio19-dev dependency
2025-04-18 23:36:49 -05:00
Jatin K MalikandGitHub a28f55b72e Add pre-commit hooks to match GitHub CI pipeline (#7)
* Add pre-commit hooks to match CI pipeline linting rules

* Add pre-commit hooks that match GitHub CI pipeline exactly

* Ran lint check
2025-04-17 00:05:34 -05:00
jatinkrmalik ee227252f2 Refactor project name from 'Ubuntu Voice Typing' to 'Vocalinux'\n\nThis commit updates all references to the project name across the codebase, documentation, and metadata. The changes include:\n- Updating module docstrings, comments, and logging messages.\n- Renaming files and directories where necessary.\n- Updating references in README, INSTALL, and LICENSE files.\n- Adjusting paths and identifiers in the code to reflect the new name.\n\nThis refactor ensures consistency and prepares the project for its new branding as 'Vocalinux'.\n\nPlease review the changes carefully to ensure no functionality is affected. 2025-04-12 00:53:48 -05:00
jatinkrmalik 40072f2e61 Add documentation and basic tests 2025-04-07 16:58:22 -05:00