Commit Graph
372 Commits
Author SHA1 Message Date
soubarnak d08cacb0b4 fix(identity): share one model-download user agent definition
Both model-download call sites built the User-Agent string separately:
recognition_manager used a hardcoded literal while settings_dialog
interpolated APP_ID. Add app_identity.user_agent() and use it in both,
with a test pinning the shared definition.

Fixes #17
2026-11-06 09:01:15 +05:30
soubarnak d7a4b8c21e feat(void): add xbps template for the voculinux app
Ticket xavierk/voculinux#27, built on #18 (rename) plus the #25/#26
dependency templates. The template builds the app from the canonical
checksum-verified release sdist, depends on Void's interpreter, GTK,
desktop bindings and packaged Python libraries, and installs the
renamed desktop entry, icons and a packaged-install marker. No speech
models, no vendored wheels, no pip at build or install time.

Packaged installs keep background update checks as a signal, but every
update affordance presents the package-manager path
(src/vocalinux/utils/packaging.py). Also corrects the Void distro
inventory (Ayatana provider name; ydotool marked absent) and regens
the installer map.
2026-11-06 03:03:33 +05:30
soubarnak 27f157355d style(models): fix isort import block in recognition_manager
Removes the blank line between the function-level imports so
isort --check-only passes on integration/spec16.
2026-11-06 01:58:07 +05:30
soubarnak 74ea855220 feat(dbus): expose dictation state signal and property
Alongside Toggle/Start/Stop, the renamed interface now publishes
dictation state (idle/listening/processing) as a StateChanged signal
and a readable State property, so status bars and scripts can follow
dictation without scraping the tray. Signals fire only on change, so
double start/stop emits no duplicates; command-line triggers behave
exactly as before.

Refs #21
2026-11-06 01:51:05 +05:30
soubarnak df771d8fb1 feat(models): memory-aware whisper.cpp ladder with conditional backend order
Free memory picks the load-time rung (large-v3-turbo-q8_0 default,
q5_0 under 1086 MiB free, small/small.en floor) for unpinned default
configs; thresholds derive from catalog sizes plus the protocol's
512 MiB headroom. A manual pin is never overridden, and a failed load
steps one rung down and retries exactly once before surfacing.

CUDA ranks before Vulkan only with a complete architecture-matching
toolkit (nvidia-smi GPU, nvcc, compute capability >= 7.0); otherwise
Vulkan, then CPU. The driver-only CUDA recommendation now demotes to
CPU logic instead of naming an unreachable VRAM ladder.

Refs #20
2026-11-06 01:48:20 +05:30
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30
soubarnak 9d09319ba1 Merge feature/23 into integration/spec16 (ticket #23: brand accent hook) 2026-11-06 00:05:37 +05:30
soubarnak c5c42fedee Merge feature/19 into integration/spec16 (ticket #19: perf baseline protocol) 2026-11-06 00:05:35 +05:30
soubarnak 203b51ab78 feat(ui): add brand accent hook to GTK colour scheme
BRAND_ACCENT single source with shade()-derived states; accent CSS
covers suggested-action buttons and the listening chip, installed at
USER priority from main().

Refs #23
2026-11-06 00:05:29 +05:30
soubarnak d7a0273da1 refactor(identity): centralise user-visible app identity
Single app_identity module owns APP_NAME/APP_ID, D-Bus names,
desktop/icon/IBus names and related surfaces; 16 consumers import
from it with zero value changes. Deliberately excludes
settings_about and dictation_pad literals pinned by existing tests.

Refs #17
2026-11-06 00:05:26 +05:30
soubarnak 6713722daf feat(perf): lock installed-build baseline protocol and take target-machine numbers
Ticket #19 (spec #16): fixed three-metric protocol as a runnable script
(idle memory both keep-alive states, wake latency from debug logs,
real-time factor on the fixed 30 s sample per backend) plus per-rung
load footprints, portal/direct injection probe, and threshold
derivation for the model-ladder ticket (#20). Offline unit tests for
the parsing/statistics/report helpers. Evidence recorded on the ticket.

One debug-only marker (Toggle requested in state ...) makes wake
latency derivable from logs alone.
2026-11-05 23:11:28 +05:30
Jatin K MalikandGitHub 0a4d637f5f fix(overlay): detect Wayland by GType so the fallback overlay stays compact (#1030) (#1035)
* fix(overlay): detect Wayland by GType so the fallback overlay stays compact (#1030)

_is_wayland() checked isinstance(display, Gdk.WaylandDisplay), but GTK3's
Gdk typelib has no WaylandDisplay class, so it always returned False. On
Wayland without gtk-layer-shell, _layout_bottom_strip() therefore sized the
prewarmed fallback window to the full monitor width (the X11 strip) instead
of the compact centered window meant for Wayland, which on KDE showed up as
a wide empty "Vocalinux Dictation" window listed in the taskbar.

Drop _is_wayland() and use _display_is_wayland(), which checks the GType
name and already gates prewarm, so both decisions agree.

Also list gtk-layer-shell as an optdepend of the source AUR package: with
it KWin and wlroots compositors get a layer surface that is never listed
in the taskbar and never needs the prewarmed toplevel. The AppImage (and
so vocalinux-bin) already bundles it.

* docs(overlay): describe the compact Wayland fallback in the overlay docstrings

The class docstring still said the overlay is always a full-width strip, which no longer holds on Wayland without layer shell (#1030).

* fix(overlay): keep the full-width strip on GNOME Wayland

Mutter ignores CENTER_ALWAYS for a plain Wayland toplevel, so the compact
fallback window landed in the top-left corner on GNOME (tested on GNOME
Shell 46 with the Ubuntu Dock). The full-width strip at least keeps the
orb horizontally centered there. Use the compact window only on KDE,
where KWin listed the strip in the taskbar as a wide empty window (#1030).
2026-11-02 19:17:27 -08:00
Jatin K MalikandGitHub f70357f2b8 fix(overlay): quit Vocalinux when the dock closes the overlay window (#1028) (#1036)
* fix(overlay): quit Vocalinux when the dock closes the overlay window (#1028)

On GNOME Wayland without layer shell, the prewarmed dictation overlay is a
regular toplevel that the dock lists as the app's window. Since #1017 the
overlay refuses close requests so it is never destroyed (#1010), which made
"Quit" from the dock silently do nothing.

The overlay still keeps its window, but now forwards the close request to
an optional on_close_request callback, scheduled once from an idle source.
The tray passes its _quit(), so the dock's Quit behaves like the tray's Quit.

* fix(overlay): drop a queued dock close once the app is quitting

A dock close request is forwarded through an idle source. If the tray's
Quit or SIGTERM tears the overlay down before that idle runs, the
callback would run _quit() a second time. destroy() now drops the
callback, and the pending flag is cleared when the request runs so a
later close can retry if quitting failed.

Refs #1028
2026-11-02 19:17:21 -08:00
Jatin K MalikandGitHub 61bfb1e58e fix(ui): escape device names and test errors in the audio status label (#1043)
The Settings audio status label is set with Pango markup. A microphone
named with '&' or '<' (or an audio test error carrying them) made the
markup invalid, so GTK dropped the text. Escape both with
GLib.markup_escape_text.

Also correct the v0.17.0 notes in docs/UPDATE.md: the Snap never
bundled VOSK; it installs the project without extras.
2026-11-02 18:46:31 -08:00
80e3019a74 feat(parakeet): add optional Underdog Bark 3.0 models, int8 and 4-bit (#1031)
* feat(parakeet): add optional Underdog Bark 3.0 int8 model

Adds bark-3.0 to the Parakeet model picker: Underdog Bark 3.0, an
English fine-tune of Parakeet TDT 0.6B v3, as the int8 sherpa-onnx
conversion published at velddev/Underdog-Bark-3.0-sherpa-onnx. It runs
on the existing sherpa-onnx recognizer, pinned to a revision with every
file checked against model_checksums.txt, like Orukeet (#840).

The conversion writes the unknown-word token where a quote mark was, so
the Parakeet transcript drops <unk>; the other models never emit it.

* docs(parakeet): say when to pick Bark 3.0; loosen its catalog test

The README now says Bark suits English dictation of conversational
speech and v3-european stays the choice for read or multilingual
speech. The test checks that bark-3.0 is offered rather than that it is
the last catalog entry, so a later model does not break it.

* test(parakeet): add docstrings to the Bark tests

* feat(parakeet): add optional 4-bit Underdog Bark 3.0 build

Adds bark-3.0-int4: the same Bark 3.0 weights quantized to 4-bit with
onnxruntime's HQQ quantizer, for machines short on memory. It needs
about half the RAM of the int8 build (0.64 GB against 1.28 GB peak) and
a 379 MiB download, at two to four times the CPU time (about 2x on
Intel, 3.7x on AMD by the publisher's measurement). The files are
published in velddev/Underdog-Bark-3.0-sherpa-onnx (Hugging Face PR #1),
and both Bark entries now pin its revision 3ae3af1, which holds both
builds; the int8 files are unchanged there.

Catalog entries gain an optional "precision" key, so the loader opens
encoder.int4.onnx and friends instead of assuming int8; every existing
model keeps the int8 default. The README section now covers both
builds in a few lines.

* test(parakeet): type the Bark test signatures

* docs(parakeet): name the Bark sherpa-onnx source and its pins

The README now says where both Bark builds download from (a pinned revision of
velddev/Underdog-Bark-3.0-sherpa-onnx), that it is an unofficial conversion, and
that each file is checked against its bundled SHA-256 pin, as the Orukeet
section does.

---------

Co-authored-by: Jatin <jatinkrmalik@gmail.com>
2026-11-02 12:31:08 -08:00
Jatin K MalikandGitHub f3913fb6bf fix(ui): show the selected microphone's full name in the device tooltip (#1029) (#1037)
The Input Device dropdown has a fixed width and cuts labels off at about
18 characters. Raw ALSA names such as "sof-hda-dsp: - (hw:0,6)" therefore
all look like "sof-hda-dsp: - (h…", and the tooltip always showed the same
generic text, so there was no way to tell which entry was selected.

The tooltip now shows the full label of the selected device. It updates
when the list is filled and every time the selection changes, and goes
back to the generic hint for System Default.
2026-11-02 12:31:02 -08:00
dbf8039196 chore(release): v0.18.5 (#1027)
* chore(release): v0.18.5

- Update version from 0.18.4 to 0.18.5
- Add v0.18.5 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema
- Add the 0.18.5 row to the RELEASE_PROCESS version history

* docs(release): qualify the clipboard-history claim for wl-copy 2.2+

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 21:40:44 -07:00
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>
2026-10-26 21:25:50 -07:00
8f220c38f4 fix(evdev): release EVIOCGRAB when forwarding cannot run (#1025)
* fix(evdev): release EVIOCGRAB when forwarding cannot run (#1021)

With the internal evdev listener, Vocalinux holds EVIOCGRAB on physical
keyboards and re-emits through uinput clones. Two windows left the grab
held while nothing could forward:

- Suspend/resume: grabs persisted through sleep while the compositor's
  input stack was rebuilt; the listener was torn down and rebuilt ~6s
  after resume, and keystrokes typed at the GNOME lock screen in that
  window went to a stale forwarding path.
- A monitor thread wedged mid-iteration (fd, lock, or a clone write that
  never returns): grabs stayed held forever because the self-healing
  path only fired on write exceptions, and the unexpected-exit cleanup
  only ran when the thread died.

_on_system_suspend now tears the shortcut listeners down before the
machine sleeps, so no grab survives the suspend boundary; the existing
resume flow rebuilds them once /dev/input settles.

A forwarding watchdog now runs alongside the monitor. The monitor stamps
a heartbeat on every loop; if it goes stale while grab->clone pairs
exist, the watchdog runs the existing _release_failed_forwarder path on
each device so input reaches applications directly, and tracks them as
suspended. Once dispatching resumes, suspended devices are removed so
the rescan re-opens them through the standard open->clone->grab path.
Generation checks keep a recycled fd from ever being torn down under a
newer generation.

* fix(evdev): re-arm watchdog-released devices on the monitor thread

The watchdog closing a device fd races the monitor's in-flight select:
a dead descriptor in the select set trips the error handler, which
exits the loop and tears down every keyboard. Recovery now runs where
the close is safe — the monitor retires each suspended device at its
own loop top and the rescan re-opens it through the standard
open->clone->grab path. The watchdog only ungrabs (safe cross-thread:
that fd never sits in the select set) and marks the device suspended.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 20:42:59 -07:00
9374b86793 fix(ui): keep standalone dialogs from binding to other windows on Wayland (#1024)
On GTK's Wayland backend, a toplevel with the DIALOG type hint and no
transient parent becomes an orphan dialog: the next toplevel to receive
keyboard focus silently becomes its xdg_toplevel parent, and GNOME then
raises, minimizes, and workspace-moves the two windows as one.

Settings, the transcript viewer, and the log viewer are standalone
windows opened parentless, so the dictation pad (focused constantly)
adopted them, making Alt+Tab group them and minimize/workspace changes
apply to both. Force the NORMAL type hint on all three so they stay
independent.

Fixes #984

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 20:28:51 -07:00
Jakob JensenandGitHub aae1f4b474 fix(clipboard): keep paste-injection copies out of clipboard history 🤖🤖🤖 (#956)
* fix(clipboard): keep paste-injection copies out of clipboard history

The clipboard-paste injection path writes the dictated text to the
clipboard only to feed the paste chord, then restores the previous
content (#588, #646). Clipboard managers such as Klipper and cliphist
still record that transient copy, so every dictation lands in history.

Mark the injection copy with `wl-copy --sensitive`, which clipboard
managers honour by skipping the entry. The flag is probed once via
`wl-copy --help` (added in wl-clipboard 2.2) so older builds keep
working. It is only set when Copy to Clipboard is off: with that
setting on, the dictated text is meant to stay, so it remains an
ordinary copy. Restoring the user's previous content is never marked
sensitive. xclip/xsel have no equivalent and are unchanged.

* test(clipboard): address review on sensitive paste copies

- Move the unittest main guard below TestSensitivePasteCopy so running
  the file directly also runs the new tests.
- Add -> None return hints to the new test methods.
- Cover _copy_to_clipboard forwarding the sensitive hint when a tool
  declines and the next one is tried.
- Docs: history suppression needs wl-copy 2.2+ and a clipboard manager
  that honours the hint; older wl-copy and the XWayland xclip/xsel path
  still produce ordinary copies.

* fix(clipboard): type the wl-copy probe cache and adapt the X11 paste test

- Declare _wl_copy_sensitive_supported as a typed class default instead
  of reading it through getattr(), which mypy (now in CI since #968)
  flags as returning Any.
- test_plain_x11_clipboard_paste_uses_xdotool_and_x11_clipboard
  asserted the exact _copy_to_clipboard kwargs. Check the text and the
  X11 tools instead, so it does not depend on the sensitive hint (which
  xclip ignores) or on the copy_to_clipboard setting.

* test(clipboard): wait for the restore thread instead of a fixed sleep

TestSensitivePasteCopy slept 0.5 s and hoped the background restore had
run. On a slow runner the patches could end first. Wait on an event set
by the restore copy, inside the patches, with a generous timeout.
2026-10-26 20:25:40 -07:00
2fce81e94d fix(shortcuts): preserve Ctrl events for mouse-wheel zoom (#967)
* fix(shortcuts): allow shortcut key passthrough

Fixes #950

* fix(shortcuts): prevent Ctrl release race during passthrough

* test(shortcuts): add type hints to passthrough regression tests

* test(shortcuts): cover passthrough edge cases and integration

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-26 20:24:14 -07:00
90adde05a4 fix(recognition): never lose a stop that races start, and read capture outside the buffer lock (#1023)
* fix(recognition): never lose a stop that races start, and read capture outside the buffer lock

start_recognition() published LISTENING (and played the start cue) before setting should_record. A stop arriving in that window, e.g. a push-to-talk release handled on another thread, was undone by the later should_record = True, leaving the capture thread recording with no way to stop it. The session is now armed under _buffer_lock before LISTENING is visible.

The capture loop held _buffer_lock across source.read_chunk(). When a read blocks (the audio server reconfigures the device, a USB/Bluetooth mic stalls), stop_recognition() gives up its bounded join and then blocks forever on that lock, stuck in PROCESSING. The read now happens outside the lock and the session is re-checked before the chunk is appended.

* test(recognition): wait for the stalled read before stopping

Signal from inside the blocking read instead of sleeping after the first, non-blocking one, so the test always exercises the stalled-read path.

* test(recognition): run the stalled-read test against the real NumPy

Older test modules leave sys.modules["numpy"] as a MagicMock. Under that leak the capture loop's first chunk raised before the stalled read was reached, so the test failed in the full-suite order on CI. Restore the real module for these tests, as test_playback_ducker.py already does.

---------

Co-authored-by: AAG Projects Team <293887937+aagprojectsteam-max@users.noreply.github.com>
2026-10-26 20:18:50 -07:00
Jatin K MalikandGitHub 750d64dc9b fix(tray): prefer libayatana-appindicator-glib when available (#1019)
Use the maintained AyatanaAppIndicatorGlib 2.0 indicator when it is installed, mirroring the existing Gtk.Menu into a Gio.Menu whose actions keep stable, never-reused names and fire the original items. Fall back to the GTK 3 indicators otherwise; the AppImage stays on its bundled GTK 3 indicator, and the .deb recommends the glib typelib.

Fixes #1013
2026-10-25 14:49:34 -07:00
Jatin K MalikandGitHub 9b4aded3a2 fix(snap): list PulseAudio microphones and explain CPU-only whisper.cpp (#1018)
Inside the snap, list the sound server's capture sources with pactl and record from the chosen one by scoping PULSE_SOURCE to each PortAudio open (serialized behind one lock). Settings saves the stable source name, keeps each dropdown entry's server name so selection never queries the server from the GTK thread, and the snap reports whisper.cpp as CPU-only instead of probing host GPU tools.

Fixes #1011
2026-10-25 14:49:25 -07:00
Jatin K MalikandGitHub ed29ddf84b fix(overlay): keep the Wayland fallback overlay alive when the dock closes it (#1017)
Refuse close requests on the pre-mapped Wayland overlay so closing its GNOME dock entry no longer destroys it and brings back the first-recording focus loss, and unmap the overlay when it is turned off so the dock entry goes away immediately.

Fixes #1010
2026-10-25 14:49:15 -07:00
62b0d157c4 chore(release): v0.18.4 (#1007)
- Update version from 0.18.3 to 0.18.4
- Add v0.18.4 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-21 01:49:03 -07:00
Jatin K MalikandGitHub 138a233892 feat(recognition): cancel an in-progress transcription with the recording hotkey (#1004)
* 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.
2026-10-21 01:33:58 -07:00
Jatin K MalikandGitHub 9b5cdf686f fix(dictation-pad): keep Test Dictation results and move pad routing to Settings (#1005)
* 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 #983
Fixes #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
2026-10-21 01:20:14 -07:00
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>
2026-10-21 01:04:10 -07:00
Jatin K MalikandGitHub f013b7ff88 fix(text-injection): route XWayland clients without IBus through xdotool (#1003)
* fix(text-injection): route XWayland clients without IBus through xdotool

On KDE Plasma Wayland with the KWin IBus bridge enabled, Vocalinux injects through IBus. An X11 client under XWayland only reaches IBus when it loaded an IBus input method module from GTK_IM_MODULE, QT_IM_MODULE or XMODIFIERS. Without one, the engine commit lands in no input context and still reports OK, so the text was lost with no fallback.

Before each WAYLAND_IBUS injection, check XWayland's active window. A real client whose own environment does not select IBus gets that segment through the WAYLAND_XDOTOOL path, and the backend returns to IBus afterwards so native Wayland apps keep using it.

Fixes #972

* fix(text-injection): honour IBus pins and the client toolkit in the XWayland detour

Keep IBus when the user pinned it, judge a GTK or Qt client only by its own toolkit's IM variable (read from the libraries it maps), and have the clipboard restore after an XWayland paste read the X11 clipboard it wrote even after the backend switched back to IBus.

* fix(text-injection): keep a non-text X11 clipboard on the XWayland paste route

WAYLAND_XDOTOOL pasted through the X11 clipboard even when it held an image or file, which it cannot save and restore. Check the clipboard targets there as the X11 route already does, and type the text instead when they hold no text.
2026-10-21 00:40:22 -07:00
4d78246174 fix(recognition): queue long dictation segments before the buffer cap (#976)
* fix(recognition): queue long dictation segments before the buffer cap

A held push-to-talk key (or a toggle session with no pause) keeps a single
segment growing until the 5000-chunk safety cap discards its oldest 25%.
The cap is a chunk count, not a duration, so the effective limit depends on
the capture format: at 16 kHz a chunk is 64 ms (5 min 20 s), but a device
that negotiates 44.1 kHz feeds 23 ms chunks and the same cap is 1 min 56 s.
A 3-minute hold therefore lost ~87 s of speech before transcription, with
only a log warning to show for it.

Flush the current segment onto the existing transcription queue once it
reaches _max_segment_seconds (90 s), or 75% of the chunk budget when chunks
are shorter, so the destructive trim can never fire on buffered speech.
Recording continues while queued segments are transcribed in order; reload
sessions keep the existing hold-until-reload behaviour.

Adds tests for the mid-hold flush, the buffer headroom, and the reload path.

* fix(recognition): hold push-to-talk text until the key is released

Text injection while PTT is held can release or modify the key.

* fix(recognition): hold PTT text without blocking the recognition worker

c7b9bd6 held transcribed text by waiting on the release gate inside
_process_audio_buffer, on the single recognition worker. Later segments
then sat in the 32-slot queue for the rest of the hold instead of being
transcribed: memory grows with every queued segment, and once 32 are
waiting (~48 min of 90 s segments) _enqueue_audio_segment starts dropping
the oldest speech.

Buffer the processed text instead. Every segment is transcribed as it
arrives; while the key is down its result is appended to a held list, and
the held results are delivered in capture order once the gate opens. The
final buffer carries the session gate too, so it cannot overtake text
that was transcribed before release. Held text is flushed when the worker
exits, at the end of stop_recognition, and after the reload drain. An
empty mid-hold result no longer flushes the held text early.

Tests assert that every queued segment is transcribed before release and
delivered in capture order afterwards.

* fix(recognition): transcribe held push-to-talk segments ahead of release

Keep transcription running during long push-to-talk holds while retaining each result until key release. Text injection during a hold can release or modify the PTT key.

* test(recognition): cover cross-session held text isolation

The per-session release gate must release only its own held text: a slow
worker from an earlier session finishing after a new push-to-talk session
starts must not deliver the new session's text while its key is down.

---------

Co-authored-by: Muhannad Abo Hajia <muhannadabohajia@gmail.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-21 00:37:43 -07:00
Jatin K MalikandGitHub e53fc2ceb0 fix(updates): follow the build channel and allow turning update checks off (#999)
Use the running build channel for background checks and add a preference to disable them.

Fixes #977
2026-10-21 00:26:09 -07:00
Jatin K MalikandGitHub 1c299d2dc7 fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend (#1001)
* fix(whispercpp): map GPU picker to ggml devices and size threads by effective backend

The GPU picker and auto selection used vulkaninfo indices, but whisper.cpp's
gpu_device is an ordinal over the GPU/IGPU devices ggml registered. ggml skips
Vulkan devices it cannot use (for example a Haswell iGPU), so the indices shift
and a selected NVIDIA card could fall back to CPU while logs claimed Vulkan.

Read ggml's device registry through the loaded pywhispercpp extension, select
and label devices by that ordinal, run on CPU explicitly when ggml has no GPU,
log the effective backend after load, and pick n_threads from the effective
backend instead of the presence of GPU libraries.

Fixes #987
Fixes #990

* test(whispercpp): move new ggml probe tests into their test class
2026-10-21 00:07:01 -07:00
Jatin K MalikandGitHub 5fa0db6a47 fix(injection): give every RemoteDesktop portal request a unique handle_token (#996)
* fix(injection): give every RemoteDesktop portal request a unique handle_token

Subscribe to predicted Request paths before calling the portal and retain the RemoteDesktop portal in Flatpak when ydotoold is unavailable.

Fixes #993

* refactor(injection): share the portal close in _demote_portal_backend

* fix(injection): recreate the portal session and keep partial counts under Flatpak
2026-10-21 00:02:28 -07:00
cf23e9d82d fix(hotkeys): monitor keyboard nodes with pointer axes ungrabbed (#978)
* fix(hotkeys): keep Logitech HID++ keyboards that report pointer axes

The pointer-axis filter from #900 drops every device that reports
REL_X+REL_Y, assuming a keyboard never does. Logitech HID++ keyboards
(MX Keys, ERGO K860) expose a single node with the same capabilities
as the mice on their receiver, so they were skipped and the shortcut
no longer fired from them.

Capabilities cannot tell them apart, so a device whose name says
keyboard ("keyboard", "keys", "K<digits>") is exempted from the
pointer-axis rule, in both the /proc and the evdev discovery paths.
The MX Master 3 on the same receiver is still skipped.

* fix(evdev): monitor keyboard-pointer combos ungrabbed

* fix(evdev): keep parse_shortcut import and drop per-rescan discovery log

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-20 23:56:51 -07:00
Jatin K MalikandGitHub 545b6d0feb fix(overlay): map the dictation overlay at startup on Wayland so the first recording keeps focus (#998)
* fix(overlay): map the dictation overlay at startup on Wayland

Mutter activates newly mapped Wayland toplevels, which can take focus from the editor.
Map the fallback overlay transparently at startup so its first recording does not remap it.

Fixes #992

* fix(overlay): detect the Wayland display by GType name for the prewarm

Detect Wayland from the display GType because GTK3 omits GdkWaylandDisplay from its typelib.
2026-10-20 23:45:57 -07:00
Jatin K MalikandGitHub d59395b5c9 fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions (#997)
* fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions

Use evdev for Flatpak Wayland/XWayland while preserving the GNOME X11 fallback.

Fixes #971

* docs(flatpak): note that GNOME X11 still needs /dev/input for hotkeys
2026-10-20 23:41:07 -07:00
Jatin K MalikandGitHub 3abc106fdb feat(languages): add Slovenian (sl) to the supported languages (#995)
Slovenian was missing from SUPPORTED_LANGUAGES, so the Settings picker did not offer it and normalize_language_shortcuts dropped any shortcut bound to it. Add it as a Whisper-only entry (whisper code sl; alphacephei publishes no Slovenian VOSK model), map the Slovenian XKB layout si to it for follow-keyboard-layout, and list it on the website languages page.

Fixes #970
2026-10-20 23:29:47 -07:00
Jatin K MalikandGitHub 65de3427fb perf: faster Settings, typing and dictation; mypy in CI, engine interface, architecture docs (#968)
* fix(perf): reduce startup and inference overhead

* fix(perf): report effective whisper.cpp fallback settings

* fix(injection): add opt-in X11 paste for long dictations

* perf(vad): lower default silence timeout to 1.2s

* fix(types): resolve mypy errors across application source

* ci: run mypy in the lint job

* ci: include mypy in lint dependency group

* fix(types): narrow evdev timestamp comparison

* refactor(recognition): add SpeechEngine protocol and engine dispatch table

* refactor(settings): move About section and engine tables out of settings_dialog

* docs: add ARCHITECTURE.md and refresh docs index and AGENTS layout

* fix(settings): preserve GTK dialog compatibility after extraction

* style: format touched Python modules

* fix(types): keep local typecheck from syncing dependencies

* test(vad): align main startup expectation with new default

* style: sort engine registry and protocol test imports

* test(settings): follow extracted About builder attribute alias

* test(settings): isolate GTK dialog base in About test

* test(settings): isolate About color scheme build test

* test(settings): restore GTK base after About scheme test

* test(settings): isolate About scheme check from GTK mocks

* test(settings): support extracted dialog mixin methods

* docs: add before/after performance benchmark charts

* fix(injection): type instead of pasting when the clipboard holds non-text data
2026-10-20 20:23:08 -07:00
2da20f791a chore(release): v0.18.3 (#964)
* chore(release): v0.18.3

- Update version from 0.18.2 to 0.18.3
- Add v0.18.3 sections to CHANGELOG, docs/UPDATE.md, metainfo.xml and website changelog
- Bump website package metadata and softwareVersion schema
- Clarify that the uinput-carrying sideload snap is the v0.18.2 asset

* docs(release): include the transcribe modal-grab fix (#965) in v0.18.3 notes

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 20:24:22 -07:00
2ec92bade7 fix(ui): release modal grab after TinyDiarize download before transcript opens (#965)
* fix(ui): release modal grab after TinyDiarize download before transcript opens

The transcribe path's ModelDownloadDialog is modal. On success it stayed
open behind the diarized transcript dialog, holding its grab so every
button on the transcript was unclickable until the hidden dialog was
found and closed. Destroy it on success there and let the transcript be
the completion affordance; the failure path still keeps the dialog up to
show the error.

* style(tests): black-format new dialog-grab tests

* fix(ui): stop download dialog pulse timer on destroy; type test helper

* style(ui): type destroy-handler varargs

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 20:24:06 -07:00
d3ba4ee743 fix(injection): send newlines as Return on the xdotool backend (#962)
* fix(injection): send newlines as Return on the xdotool backend

xdotool type maps '\n' to the Linefeed keysym, which GTK targets
silently drop — "new line" produced nothing on X11 even though the
segment reached the injector.  Split each chunk on '\n' and press
Return between typed segments so a newline lands as a real keypress
in every app.

Verified against a live GtkTextView on Xvfb: the same "A\nB" input
that previously produced 'AB' now produces 'A\nB'.

Fixes #955

* fix(commands): consume trailing punctuation after mid-utterance commands

"New paragraph." inside a longer utterance substituted correctly but
left the engine's sentence-final period typed after the break
("\\n\\n. Does this work now?").  Text-command substitution now
consumes a run of trailing , . ! ? ; : … when it precedes whitespace
or the end of the segment — the reporter's comma-phrased case
("…, new paragraph, …") stops leaking the stray comma the same way.
A mark glued to following text ("newline.io") is untouched.

Fixes #955

* fix(injection,commands): resume mid-chunk on retry, don't eat produced marks

Greptile review follow-ups on #962:

- A failed Return mid-chunk left 'typed' at the chunk start, so the
  retry re-sent text xdotool had already delivered.  'typed' now
  advances per typed segment and per Return keypress, so the retry
  resumes exactly at the un-typed position — and the clipboard
  fallback reports the same progress if retries run out.
- _TRAILING_PUNCTUATION could consume a mark produced by an earlier
  command replacement ('hello new line exclamation mark' gave '\\n'
  instead of '\\n!').  The eater now only consumes marks before
  whitespace; the end-of-utterance case ('hello new line.') is a
  pre-pass on the raw segment before any substitution runs.

* style: black-format comment indent and test condition

* fix(commands): strip segment before the end-of-utterance cleanup

Trailing whitespace ('hello new line. ') hid the final mark from the
pre-pass: the raw segment ended in a space, and the stripped segment
left the dot at end-of-string where the mid-utterance eater cannot
touch it.  Strip first, then run the cleanup.

Also add the missing type hints on the new test helper.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 19:05:34 -07:00
devin-ai-integration[bot]andGitHub e866cb85e4 fix(hotkeys): prefer evdev on GNOME X11 and skip redundant IBus engine restore (#963)
Fixes #954.

GNOME X11: mutter never delivers physical key events to the XRecord extension pynput relies on, so the listener started cleanly but shortcuts stayed dead. Prefer the evdev backend there (same path as Wayland), falling back to pynput when /dev/input is unusable.

Shutdown: `ibus engine <name>` rewrites the whole X server XKB map, and the unconditional restore collapsed GNOME's multi-layout map and broke layout switching. The daemon check now uses _query_active_engine() (no GNOME gsettings fallback) so a cleared engine reads as None; the restore write is skipped when a real engine is live or the target is an xkb:* source GNOME owns, and still runs for real IM engines (#558).
2026-10-16 09:48:28 -07:00
0ddb127437 fix(commands): keep line breaks from standalone voice commands (#958)
* fix(commands): keep line breaks from standalone voice commands

Two stacked defects ate the newline "new line" / "new paragraph"
produce when spoken alone:

* _normalize_segment_text stripped all leading whitespace, so the
  command's leading newline never reached the injector (a clean
  "new line" segment was dropped entirely).
* whisper.cpp transcripts arrive punctuated ("New line."), which
  missed the exact-match ladder and left a stray "." after the
  substituted break, injected as ". ".

Normalize now strips only spaces/tabs, and the generic path
substitutes on a punctuation-stripped core when the whole segment
is command phrases, so engine edge punctuation cannot leak while
real text keeps it.

Fixes #955

* fix(injection): deliver newline-only segments to the focused app

inject_text treated whitespace-only text as empty and returned early,
so the "\n" a standalone "new line" now correctly produces was
discarded before any backend ran ("Empty text provided, skipping
injection").  Only a truly empty segment is skipped; whitespace-only
input is typed/pasted verbatim (xdotool types "\n" as Return).

Space/tab-only segments never reach this point: segment normalization
strips them upstream.

Fixes #955

* fix(injection): keep skipping space/tab-only segments

The previous commit let every whitespace-only string through, which
also delivered bare spaces and tabs a caller never wants typed.
Narrow the skip to text with no content outside space/tab so "\n"
still reaches the backend while "   " stays a no-op (the pre-existing
test_empty_text_injection contract).

Fixes #955

* fix(injection): deliver newline segments over IBus too

IBusTextInjector.inject_text had the same whitespace guard and dropped
"\n" before it reached the engine socket; narrow it the same way so
a newline-only segment is sent while space/tab-only stays a no-op.

Also let capitalize_sentences uppercase the first letter after leading
whitespace, so Vosk + auto_capitalize keeps working on "\n hello".

Covers Greptile's findings: real-socket test for the IBus path, the
capitalize regression via the main() callback seam, and type hints on
the new injector tests.

Fixes #955

* test(injection): add type hints to new IBus socket test

AGENTS.md requires hints on every signature; the Greptile review
flagged the new test for missing them.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-15 16:54:05 -07:00
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
0faac6b3b8 chore(release): v0.18.2 (#943)
* chore(release): v0.18.2

* chore(release): add Flatpak release notes and refresh changelog date

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-13 00:00:20 -07:00
devin-ai-integration[bot]andGitHub cd388b2e93 fix(evdev): strip SW_RFKILL_ALL from uinput clone capabilities (#947)
The evdev uinput clone copied EV_SW capabilities verbatim. "ThinkPad Extra Buttons" (thinkpad_acpi) declares SW_RFKILL_ALL, and a fresh uinput device reports every switch as 0. The kernel rfkill-input handler reads a 0 killswitch as radios-off, so every clone creation soft-blocked all radios at startup and again after every resume.

Strip SW_RFKILL_ALL from the clone capability list, dropping EV_SW entirely when it was the only switch. Other switches stay declared so real switch events still forward; a forwarded SW_RFKILL_ALL write is dropped by the kernel for the undeclared code.

Fixes #946
2026-10-12 18:07:50 -07:00
AlexThunder1989andGitHub b0fefd6ec1 fix(evdev): leave held keyboards ungrabbed at startup (#941)
Solid fix, this was leaving keys stuck on the desktop whenever someone was typing while the app started.
2026-10-11 23:50:43 -07:00
AlexThunder1989andGitHub 9d667dba3f feat(overlay): live mic orb with audio-reactive pulse and ripple (#940)
Looks great, the live level feedback makes the overlay feel a lot more responsive.
2026-10-11 23:07:07 -07:00