- Engine checklist and results table now split by a Gtk.Paned (vertical)
so the user can drag the divider to give more room to either panel
- WAV and reference .txt paths are written to disk (save()) the moment a
file is picked via the file chooser, without needing to click Save;
also saved on Run if they changed since the last disk write
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Benchmark tab: scrollable engine checklist above Run with live
reachability dots (green/red), name+model+URL filter, All/None buttons;
_run_bench respects selection and shows clear message when nothing ticked
- About tab: changelog now rendered via _md_panel (markdown headers, bold,
lists) instead of plain monospace _text_panel
- Manual tab: graceful fallback with clickable GitHub link when MANUAL.md
is not installed; build-deb.sh now copies MANUAL.md from repo root so
/opt/blitztext/MANUAL.md exists in future installs
- STT quickstart templates expanded: Speaches docker, whisper.cpp server,
NVIDIA NIM/Parakeet, five built-in local model sizes (tiny→large-v3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap ListStore in TreeModelSort and set sort_column_id on every column
so clicking any header sorts ascending/descending.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Engines tab: move Test button out of the toolbar, place it in a row
directly beside the result label so button and output are co-located;
result label is now selectable so error text can be copied
- Benchmark tab: add bench_wav/bench_ref/bench_expand_models to Config;
file pickers restore last-used paths on open; any change (file-set,
toggle, or Run) writes directly to cfg so paths survive without Save;
[benchmark] section written to config.toml on Save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- stt.detect_remote_device(): probes /info (faster-whisper-server) then
/metadata (NVIDIA NIM) to detect CUDA vs CPU; cached per unique URL
- BenchRow gains url field; Device column now shows "CUDA" for GPU remotes
instead of the generic "remote"
- Benchmark table gains URL column (scheme stripped, max 180px wide)
- "Test all models per engine" checkbox: fetches list_models() for each
remote engine and expands to one row per model when checked
- benchmark.run() gains expand_models parameter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _run_bench: deduplicate STT engines by name before running; show a
warning in the summary line listing which names were skipped
- _bench_add_row: replace raw HTTP error strings with human-readable
reasons ("Wrong model name", "Server offline", "Timed out", etc.);
full raw error stored in hidden column 7 shown as row tooltip on hover
- bench_store: added 8th column (tooltip text); tree.set_tooltip_column(7)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
install -m644 guarantees world-readable regardless of umask, so
`sudo apt install ~/blitztext_*.deb` works without a manual chmod.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BenchRow gains `best_for` field: "Short clips" / "Short / medium" /
"Long / batch" / "Streaming" — derived from engine type and model name
- Device now shows "CUDA" instead of "GPU" for clarity
- Benchmark table gains a "Best for" column between Device and Time(s)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- stt._transcribe_remote: no longer falls back to "whisper-1" when
engine.model is empty — omits the field entirely so Riva/NIM uses its
default model instead of rejecting the request with HTTP 400
- gtksettings._page: switch vertical scroll policy to ALWAYS and disable
overlay scrolling so the scrollbar is permanently visible, making
it obvious when a tab has more content below the visible area
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- stt.list_models(): fall back to /metadata (NVIDIA Riva/NIM) when /models
returns nothing — extracts shortName and strips the version tag suffix
- Settings: show "type model name manually" placeholder when fetch returns
empty and no model is currently set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
numpy is a runtime dep bundled in the .deb venv but not in the dev
.venv, so Pylance can't resolve it at edit time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of session freeze confirmed: a 15 000-char code block was typed
character-by-character via xdotool at 12ms/char = ~3 min, flooding the X11
per-client event buffer until the entire session froze.
- paste.py: any text >300 chars or containing newlines auto-upgrades to
clipboard paste (instant Ctrl+V) regardless of configured output mode.
xdotool type is kept only for short single-line text where it matters.
- daemon.py: replace on_token "".join(acc) accumulation (O(n²) for long
code blocks) with a sliding deque that shows only the last 400 chars in
the overlay — Pango no longer re-lays out a growing 15 KB string on each
incoming token.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- daemon: GLib.timeout_add (thinking animation) was called from a background
thread — NOT thread-safe in PyGObject/GTK3 and the prime suspect for the
session-level freezes. Fixed via idle_add so timeout_add runs on main thread.
- daemon: add SIGQUIT handler — kill -QUIT <pid> dumps all thread stacks to
the log so the freeze location can be identified without rebooting.
- daemon: VAD hard-timeout safety net — if LevelMeter fails to open the mic
(device busy because wakeword pw-record holds it), dictation now auto-stops
after 30s instead of hanging forever. Wakeword sessions also capped at 60s.
- overlay: coalesce set_level() calls the same way set_text() is coalesced,
so mic level spikes from 3 simultaneous pw-record streams can't flood the
GTK main loop either.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- llm.py: catch OSError/TimeoutError (socket.timeout is NOT a URLError subclass)
and bare Exception so no path kills the background thread silently
- daemon.py: catch unexpected exceptions from llm.chat and surface them as
overlay errors instead of silent thread death
- daemon.py: reject LLM responses that are >80 % whitespace (cold-start
model returning spaces/blank lines gets discarded, not typed)
- daemon.py: show transcribed text in overlay immediately before thinking
animation so user can see what was heard while LLM processes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add expand_spoken_punctuation() in quality.py: converts spoken phrases
"new line" / "neue Zeile" → \n and "new paragraph" / "neuer Absatz" → \n\n
before text is typed; xdotool/wtype handle \n as Return
- Daemon: pulse "⏳ Thinking..." animation in the overlay while waiting for
the first LLM token (400ms interval), replaced automatically when streaming
starts so the user sees activity during cold-start latency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add llm_engine field to Workflow (config.py load/save)
- Daemon picks the named engine per-preset, falls back to active engine
- Settings Presets tab: replace free-text model field with engine dropdown
populated from configured engines; "(active engine)" = default
- Fix LLM token streaming crash: coalesce GLib.idle_add calls so only
one flush is ever queued (prevents GTK main loop flooding)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
caret: change default overlay_anchor from "caret" to "pointer" — AT-SPI's
synchronous D-Bus extents read on the GTK main loop could freeze the entire
X11 session when the focused app was slow to respond. Pointer anchor is a
reliable fallback (the mouse is almost always near where you're typing) and
avoids AT-SPI entirely by default.
caret: wrap the AT-SPI rect() read in a daemon thread with a hard 300ms
timeout — so even if overlay_anchor="caret" is configured manually, a frozen
or unresponsive target app can never stall the GTK main loop.
settings: replace the Manual tab's plain TextView with a styled markdown
renderer (_md_panel): # h1/h2/h3 headings, **bold**, *italic*, `inline code`,
> blockquotes, --- rules, bullet/numbered lists, and | tables |.
settings: infobox background changed from theme-adaptive grey (unreadable on
dark themes) to a fixed blue tint (rgba 66,133,244 @ 9%) with a matching blue
border — readable in both light and dark themes.
settings: active notebook tab now shows bold text with a #1a73e8 blue colour
and extra padding, making the current tab obviously distinct.
settings: emoji picker buttons enlarged to 20 px / 38×38 px minimum (grid)
and 18 px / 34×34 px (category bar) via bt-emoji-btn / bt-emoji-cat-btn CSS
classes — previously rendered at the default button label size (~11 px).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Emoji picker: SearchEntry at top filters all categories via unicodedata.name()
in real time; category view hides while searching, restores on clear
- Manual tab: add pkg_dir/MANUAL.md to _app_paths() search list so it works in
both venv and deb installs (MANUAL.md deployed alongside the package)
- bt-infobox CSS: replace @theme_selected_bg_color (saturated blue) with a
neutral 5% mix of fg/bg so banner text is always readable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Every _labeled and _switch_row field now gets a clickable ⓘ info button
that opens a plain-language help popover — targeted at non-technical users
- New Manual tab in Settings renders MANUAL.md directly inside the dialog
- STT and LLM toolbars each get a "Quickstart ▾" button: a menu of common
providers (OpenAI, Groq, OpenRouter, Ollama, LM Studio, vLLM, llama-swap,
faster-whisper-server, NVIDIA Riva) that pre-fills the engine form in one click
- Engine type combos now show human-readable labels ("Internal — faster-whisper",
"LAN server — runs on your machine", "GPU (CUDA)", "int8 — fast, less memory")
while storing the same internal key values (no config migration needed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the 60-emoji flat list with 8 standard Unicode categories
(Smileys, People, Animals, Food, Travel, Activities, Objects, Symbols),
each with 30–100 emojis. The picker now has a category tab bar at the top
(emoji icons, highlighted active tab) and a scrollable emoji grid below —
matching the layout used in WhatsApp and other messaging apps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a 😀 button next to the Icon (emoji) entry in Settings → Presets.
Clicking it opens a GTK popover with 60 common emojis in a scrollable
flow grid; selecting one writes it into the field and closes the picker.
The entry still accepts direct keyboard input as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Python 3.12 the 'except ... as e' variable is deleted after the except
block exits, so a plain lambda that closes over 'e' raises NameError when
GLib fires it later. Bind e as a default argument at lambda creation time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Globs don't expand in redirect targets. Use the venv Python to resolve the
exact site-packages path instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Debian/Ubuntu, python3-gi is installed into the non-versioned
/usr/lib/python3/dist-packages path which venvs exclude even with
--system-site-packages. A debian-system-packages.pth file adds it to
sys.path so `import gi` works from the venv.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'python -m blitztext' adds cwd to sys.path, so running blitztext from inside
a directory that contains a blitztext/ folder shadows the installed package.
A proper shebang entry-point script sets sys.path[0] to the script's own
directory (venv/bin), making the launcher cwd-independent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each apt package must be a separate array element; passing them as one quoted
string caused apt-get to fail with "unable to locate package <all names>".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Check for python3-gi (PyGObject), xdotool, libnotify-bin, and pipewire-bin
before creating the venv; if any are missing, run sudo apt-get install
automatically so the install works out of the box on a fresh system.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the venv out of the source tree to ~/.local/lib/blitztext so it works on
CIFS/SMB shares (--copies avoids the lib64 symlink that CIFS rejects). Copies
the blitztext package into site-packages so the cloned source folder can be
deleted immediately after running install.sh. Adds a ~/.local/bin/blitztext
launcher and installs hicolor icons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All rows in the Input tab (labeled, key_field, url_field, sound_field) now share
a single LW=175 label width, eliminating the jagged left edge on input fields.
Added X-GNOME-Autostart-Delay=12 to the .desktop entry so gnome-shell, AT-SPI,
and the input stack are fully initialised before Blitztext connects — fixes the
session crash on first login after installation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gtksettings: build each notebook tab on first view instead of all up front, so
the dialog opens instantly (was ~1.3s building Input/Benchmark file-choosers);
_collect() force-builds unvisited tabs before saving so no field is missed.
- gtksettings: connection dot now sits left of the URL entry (like the Engines
tab) instead of at the far right.
- gtkui: open_settings raises the existing dialog instead of opening a second.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Green/red/grey reachability dot next to the wakeword and TTS endpoint fields,
matching the existing STT/LLM engine dots. Lightweight background TCP probe,
refreshed on open, on reload (⟳), and on focus-out — never blocks dialog build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On headless/minimal desktops the gvfs org.gtk.vfs.UDisks2VolumeMonitor dbus
service often fails to activate; each Gtk.FileChooserButton then blocked ~25s on
a StartServiceByName timeout while realizing, so the Settings dialog never
appeared and the stalled main loop froze the control panel too. Set
GIO_USE_VOLUME_MONITOR=unix in run_gui() before any window is realized.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Send by voice: a configured edge-anchored phrase (e.g. "computer send") is
stripped and the rest is delivered AND submitted with Enter. Off by default;
[routing] send_keywords + Settings → Input.
- Wakeword benchmark (Settings → Benchmark): synthesize the wake phrase in
random voices via any OpenAI-compatible TTS server, stream to
wyoming-openwakeword, report recall / false-fires / per-voice breakdown.
New [tts] config block.
- Tests: test_voice_send.py, test_wakeword_bench.py.
- Ignore agent workspace folder jules/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live waveform and silence auto-stop countdown were driven by a level
meter that was the last user of sounddevice/PortAudio, which hangs opening
the default input on PipeWire systems — so both stayed blank on the hotkey
and wakeword paths alike. Rewrite LevelMeter to stream raw PCM from the same
recorder as the WAV path (pw-record/parecord/arecord) and RMS it; identical
API, scaling, and ~10 Hz cadence. Also fixes the Settings mic-level preview.
Set GLib prgname/application name to "Blitztext" before any window is
realized (and add StartupWMClass to the .desktop) so the taskbar and GNOME's
"… is not responding" dialog show the app name instead of "__main__.py",
without touching the `python -m blitztext` entry point.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a configurable voice cancel: saying "abbrechen" (or "cancel") at the start
or end of a clip discards the whole dictation — it is never routed onward,
rewritten, or typed. The rescue for accidentally triggered (e.g. wakeword)
recordings. Matched the same edge-anchored, ASR-tolerant way as routing keywords
via routing.is_cancel(), so the word buried mid-sentence won't trip it; checked
in Daemon._process right after transcription, before routing/rewrite/delivery.
Configurable via [routing] cancel_keywords (default ["abbrechen", "cancel"];
empty disables) and Settings -> Mic/Cues -> "Cancel words". The overlay briefly
shows "Abgebrochen". Docs (both READMEs) and CHANGELOG updated; tests cover the
matcher, the config round-trip, and the discard/deliver pipeline branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix a desktop-session freeze (forced logout/reboot) caused by the overlay's
AT-SPI caret tracker: it subscribed to the high-frequency object:text-caret-moved
signal and made synchronous, blocking AT-SPI reads from inside the event handler,
re-entering the a11y dispatcher and getting stormed by the app's own xdotool
typing until GNOME stopped responding. Now track focus changes only and read the
caret rectangle lazily, once, when the overlay shows — never on the hot path.
Fuse voice-routing feedback into the overlay instead of a desktop notification:
show the matched preset's emoji, name, and spoken keyword on a banner, narrate
the phase (Transcribing -> Rewriting), and stream the LLM rewrite into the bubble
token-by-token. Redundant per-dictation notifications are suppressed when the
overlay is present (errors still notify); headless/overlay-off is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A full circle wraps the mic glyph and drains clockwise as the trailing-
silence timer runs out, recolouring cyan→amber→red and emptying exactly
as auto-stop fires. The daemon emits the countdown from the same VAD loop
that decides auto-stop, so the ring stays in sync; the overlay drains it
against its own clock for smooth motion and fades it in/out so word gaps
don't flicker it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show a translucent, click-through bubble the moment recording starts (by
hotkey or wakeword): a pulsing microphone, a live waveform of the mic
level, and the recognised text (word-by-word when streaming, otherwise a
brief final-result confirmation). The tail points at the text caret via
AT-SPI accessibility, falling back to the mouse pointer, then a screen
corner. Also gives hands-free wakeword sessions visible feedback, whose
notifications are suppressed by design. X11 only.
- overlay.py: GTK override-redirect HUD (mic + waveform + bubble), drawn
with Cairo; thread-safe, marshalled onto the GTK loop.
- caret.py: best-effort anchor (AT-SPI caret -> pointer -> window/corner).
- daemon: optional level_cb/text_cb hooks; reuses the VAD level meter for
non-streaming, a dedicated meter for streaming. Stays UI-agnostic.
- gtkui: instantiate the overlay, drive show/update/hide from status.
- General settings: "Visual overlay" toggle; config overlay_enabled /
overlay_anchor (default on, "caret").
- Docs: CHANGELOG 1.5.0, version bump, README + MANUAL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Spoken presets: route() now also matches a preset's name as an implicit
keyword, so presets with no configured keywords (Nicer email, Calm down,
Add emojis) are triggerable by voice — say the name. Explicit keywords still
win; names are added to STT hotwords too. Tests added.
- General tab: switches moved to the far right of each row with an inline
grey description (new _switch_row helper) so each toggle is self-explanatory.
- About tab: add "Copyright: 2026 mARTin Bierschenk - Design".
- MANUAL + CHANGELOG updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump version to 1.4.0 and cut the [1.4.0] - 2026-06-07 changelog section:
- "Announce matched preset" notification (shown even hands-free) + per-preset
emoji icons.
- Voice-routing no-keyword default now prefers a transcribe preset instead of
the first preset.
Refresh the README status line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The matched-preset feedback used _dnotify, which is suppressed for hands-free
sessions — so wakeword users never saw which keyword/preset fired.
- Add a dedicated "Announce matched preset" notification (_rnotify), gated by
a new [general] notify_routing flag (default on) and independent of the
hands-free silence, so it shows for wakeword commands too. It only fires on a
real routing match, so it never spams when nothing is said.
- Show each preset's emoji in that notification; add a per-preset "Icon (emoji)"
field to the Presets editor so matches are visually distinct.
- General-tab toggle; MANUAL + CHANGELOG updated. Adds a test that the match is
announced even when _session_silent is set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When no [routing] default is configured, default_preset fell back to
workflows[0]. If that was an LLM rewrite (e.g. "Improve text"), every
wakeword/voice command without a matching keyword was sent to the language
model — repeatedly failing with HTTP 502 when the LLM backend was down.
default_preset now prefers a transcribe-mode preset for the no-keyword
fallback, so the default action is plain transcription. Adds tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump version to 1.3.0 and cut the [1.3.0] - 2026-06-07 changelog section
(wakeword pause toggle, independent hands-free audio cues, configurable
auto-stop silence, notification hygiene, PortAudio noise suppression, and the
new MANUAL.md). Refresh the README status line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported issues from hands-free use:
- Wakeword WAVs didn't play: the new "Play audio cues" master switch
([sounds] enabled) also gated the hands-free Sound: detected/captured cues,
so enabled=false silenced them. The cues live in a separate UI section, so
this was surprising. Wakeword cues are now independent of that switch: they
play whenever a file is set, and an empty field means silent (no system-chime
fallback) — which is also how you turn a hands-free cue off. The master switch
now governs only the manual (keyboard) before/after chimes.
- Clarified the four sound fields' tooltips/labels (detected/captured = hands-free
only; before/after = manual only) and the empty-field behaviour.
- New "Silence to stop (s)" setting (Settings → Input → Hands-free, or
[wakeword] silence_seconds, default 2.0): user-defined trailing-silence
timeout for hands-free auto-stop (was hard-coded to 2.5 s).
- Add MANUAL.md documenting every setting in every tab; link it from the README.
Tests: cue independence + manual-gating + roundtrip (17 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups from hands-free testing:
- Audio cues had no off-switch: empty sound fields fall back to the freedesktop
system chime, so wakeword/recording always made noise. Add a [sounds] enabled
master flag (default true) exposed as "Play audio cues" in Settings → Input.
When off, _play_cue/_play_sound are no-ops — fully silent operation.
- The VAD level meter (sounddevice/PortAudio) leaked harmless thread-teardown
errors ("pthread_join ... failed", "PaUnixThread_Terminate ... failed") to the
terminal on every clip end. PortAudio writes these straight to fd 2, so wrap
the stream open/close in a fd-level stderr suppressor (_quiet_c_stderr).
Tests: cue gating respects the master switch (16 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the cramped thumbnail tables in the root and linux READMEs with one
full-width image per Settings tab (Presets, Engines, Input, Wakeword, General,
Benchmark, Log, About), each followed by a caption explaining what the tab does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two reported bugs:
1) Notifications still appeared during hands-free use while away. A wakeword
hit arriving while the previous clip was still transcribing went through
toggle(), which popped a "Busy" notification — the away-from-keyboard
notification storm. Wakeword triggers now call start_dictation(silent=True)
directly, so a busy/not-ready state is ignored silently. Transcription and
rewrite failures during a silent session are now logged instead of raising
critical desktop notifications.
2) "Wakeword did not work" with the log line "muted via /tmp/wake_muted": a
stale mute flag silently disabled detection. The tray "Pause wakeword"
toggle (already added) clears it; the daemon now also logs a clear
"Starting PAUSED" warning when it boots with the flag present.
Adds tests/test_daemon_notify.py covering silent-session suppression and the
busy-while-wakeword no-notify path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: a stale /tmp/wake_muted flag silently disabled wakeword detection
with no in-app way to clear it (the flag was read but never written by
any code or UI).
- wakeword.py: expose MUTE_FILE + is_muted()/set_muted() helpers
- tray.py: add a "Pause wakeword" CheckMenuItem (shown when wakeword is
enabled) that toggles the flag and reflects current state
- config.py: document the tray toggle instead of the bare /tmp file
- CHANGELOG: record the wakeword pause toggle and the existing notify
hygiene fixes (transient/single-bubble notifications, silent
hands-free sessions)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refresh the panel/tray/settings screenshots, normalise "tray menu.png" to
tray-menu.png (drop the old "panel resorted.png"), and add a hero image plus a
Screenshots section (settings tabs) to linux/README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_exec_command now detects whether Blitztext is running from a source checkout
(not /opt or site-packages) and, if so, writes an autostart entry that launches
that exact source with PYTHONPATH — so the "Launch on login" toggle no longer
reverts to a stale installed .deb when developing from source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two optional WAVs in the Hands-free section: "Sound: detected" plays when the
wakeword fires (your cue to speak now) and "Sound: captured" plays when the
command is taken (silence/stop). For hands-free sessions these take precedence
over the general [sounds] cues, falling back to them and then to the built-in
system sound. The general cues are relabelled "Audio cues (manual dictation)".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A session started by the wakeword is marked silent: its per-dictation
notifications (Recording, Transcribing, routing match, Rewriting, done) are
suppressed via a new _dnotify helper. Keyboard/GUI-triggered dictation still
notifies normally, and real errors still show.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- notify-send notifications are now transient (boolean:transient:true) with a
2.5s expiry, so they no longer pile up in the message tray / lock screen.
- Single-instance guard (abstract unix socket) in the entry point: a second
`tray`/`gui`/`run` launch exits instead of starting a duplicate daemon — the
duplicate caused two wakeword listeners + two recorders.
- Empty/quiet/keyword-only results now only log, instead of firing a desktop
notification on every (often false) wakeword trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a plain-language info box to the top of every Settings tab (Presets,
Engines, Input, General, Benchmark, Log, About), exposed to screen readers, and
tooltips (wired to ATK descriptions) on the preset, general, and input-mode
controls — for non-technical and blind users (barrierefrei).
Complete the unreleased 1.2.0 changelog with the engine manager, Benchmark tab,
custom audio cues, Log tab, click-to-bind hotkeys, the GTK rebuild, voice
routing, the modifier hotkey scheme, and the quality gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New sound.py plays a user WAV (or a built-in system sound) without blocking.
Config gains [sounds] before/after paths. The daemon plays the "before" cue when
recording starts and the "after" cue on every stop (stop+paste, stop+paste+Enter,
or auto-stop on silence — all funnel through finish_dictation). Input tab gets an
Audio cues section: file pickers with play-test and clear-to-default buttons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Wayland support: text delivery using wtype or ydotool (paste.py)
- Testing: added Pytest coverage for routing, quality, and config logic
- CI/CD: added test-linux job to .github/workflows/ci.yml
- Feature: Hands-free dictation using an external wyoming-openwakeword
server, respecting the /tmp/wake_muted toggle.
- README.md: full rewrite with architecture, quickstart, config examples,
privacy model, and contributing section (deduced from source)
- recorder.py: close leaked fd from mkstemp (SEC-2)
- gtkui.py: guard next() with default to prevent StopIteration (CQ-4)
- dependabot.yml: add pip ecosystem for linux/ deps (SEC-5)
- BenchRow gains a device field; Transcriber records its resolved device, so
local engines report CPU or GPU and remote engines show "remote". New Device
column in the results table.
- Accuracy is now case-sensitive by default (capitalisation counts) so an
all-lowercase transcript no longer scores 100%. Punctuation is still ignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New benchmark.py: word-error-rate accuracy + a run() that times each STT engine
on a reference clip. Settings gains a Benchmark tab: pick a .wav and a matching
.txt, run all STT engines, see Time + Accuracy per engine and a summary of the
fastest and most accurate. Add presets for each model you want compared.
Fix "Local engine selected but the model isn't loaded." on Test: a cached
_transcriber_for() loads the local model on demand (the daemon only preloads it
when a local engine is active). Tested: local small 2.17s/100% vs remote :8010.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both STT and LLM engine editors gain a Name field (first field). Editing it
renames the engine and updates the dropdown on commit; Save persists it. Fixes
the inability to rename "New STT"/"New LLM" after Add.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Local-Whisper Device + Compute type now live under the STT engine section
("Local engine — device & precision"), removed from the General tab. The
local engine's Model field shows the effective whisper size.
- LLM engines gain a Type field (local | cloud), mirroring STT, stored in config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New logbuffer.py captures app messages (log()) and library logs (faster-whisper,
huggingface_hub) into a ring buffer, mirrored to stderr. Settings gains a Log
tab: monospace view, 1s refresh, auto-scroll, Copy, Clear — so model
download/load progress is visible instead of an opaque "Loading…". transcribe
and daemon now log via the buffer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the model combo with a ModelPicker: an editable field plus a ▾ button
that opens a popover with a SearchEntry on top and a scrollable, filtered list
of models (HuggingFace-style). Add a refresh icon next to each engine URL that
loads models from {url}/models on demand. Verified: 8010 -> 428 models, typing
"turbo" filters to the turbo variants. You can still type a custom model name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each key field (Start/Stop+paste/Stop+paste+Enter/Cancel, and the preset
Hotkey) gets a Set button that captures the next keypress and writes it in the
config format. Captures modifier-only chords (e.g. Ctrl+Win -> <ctrl>+<cmd>) by
accumulating pressed keys and finalising on first release; key events are
swallowed while binding so they don't leak into the UI. Tested: token mapping,
combo formatting, and the bind flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The engine Model field is now an editable combo with a type-to-search
EntryCompletion (substring match). When you enter a remote engine URL it fetches
the model list from {url}/models (OpenAI `data[].id` or Ollama `models[].name`)
and fills the dropdown; local STT offers the whisper sizes. stt.list_models
added; tested live (8010 -> 428 models, 28080 -> 18, 19001 -> 1). You can still
type a custom model name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
set_placeholder_text on the preset and engine entries (URL, model, API key env,
keywords, hotkey, temperature, language) so empty fields show light-grey
examples of what to enter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New GTK settings with a dropdown+editor pattern across four tabs:
- Presets: select/add/delete prompt presets; edit name, keywords, hotkey, mode,
per-preset model/temperature, and a clearly-bordered prompt textfield.
- Engines: STT and LLM engine managers with green/red online-offline status
dots, add/edit/delete, active selector, and an STT record-and-benchmark Test.
- Input: input scheme + keys + quality-gate settings.
- General: microphone picker with a live sounddevice level meter, output,
language, notifications, launch-on-login toggle, and local-Whisper settings.
Adds audio.py (mic enumeration via pactl + LevelMeter) and autostart.py
(~/.config/autostart entry). Recorder + daemon honour the selected mic.
Requirements gain sounddevice + tomli-w.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New quality.py: analyze the recorded WAV (duration + RMS, stdlib only) and drop
clips that are too short or too quiet before transcribing; after transcribing,
reject the stock phrases Whisper invents on silence ("Thank you.", "Untertitel
…", "Vielen Dank." etc.) on short clips. New [quality] config section
(min_speech_seconds, silence_rms, reject_hallucinations, strip_trailing_punctuation).
Tested: silence rms 38 rejected, speech rms 4656 passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New inputmode.py: Ctrl+Win start, Ctrl stop+paste, Alt stop+paste+Enter, Esc
cancel; toggle and push-to-talk variants. Low-level pynput Listener with arming
so stop/send/cancel only fire while recording and only after the start chord is
released — a stray Ctrl tap while idle does nothing (unit-tested).
Daemon split into start/finish/cancel_dictation (+ send_enter -> press Enter),
with start_input/stop_input dispatching between "modifiers" and "hotkeys" modes.
New [input] config section; paste.press_enter for auto-send. Default mode is
"modifiers"; voice-keyword routing still applies to what you say.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- stt.py: local + remote (OpenAI-compatible) transcription, TCP reachability
status, benchmark. Tested live against faster-whisper-server :8010.
- llm.py: LLM engine presets (url/model/key/temperature) + chat + status.
Tested live against local Qwen :19001 and llama-swap :28080.
- config: [stt]/[[stt_engine]] and [llm]/[[llm_engine]] presets with active
selectors; defaults synthesized from legacy [whisper]/[rewrite].
- daemon: transcribe via active STT engine, rewrite via active LLM engine;
load local Whisper only when the active STT engine is local.
- Rename package entry __main__.py -> blitztext.py (thin __main__ shim keeps
`python -m blitztext` working). Remove rewrite.py (folded into llm.py).
- App icon extracted from the macOS AppIcon.icns; installed at several hicolor
sizes by the .deb and used as the window icon. Drop placeholder SVG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One hotkey (Ctrl+Alt+Space) records; the spoken keyword at the start or end of
speech selects the preset, which is then stripped and the rest applied. New
routing.py does ASR-tolerant matching (normalize + fuzzy + edge-window scan +
token-drift slack); config gains a [routing] section and per-preset `keywords`;
the daemon adds a "route" mode and biases Whisper with the keywords as hotwords.
Falls back to a default preset when no keyword is recognised.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the tkinter panel with a GTK3 control panel (gtkui.py) + settings
dialog (gtksettings.py): rounded CSD window, gradient background, circular icon
avatars, hotkey pills, hover states, Ubuntu font. Runs on the GTK main loop,
unified with the AppIndicator tray (no tkinter event-loop pump). Status updates
marshalled via GLib.idle_add. Removes the old tkinter gui.py.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
packaging/build-deb.sh builds an installable blitztext_<ver>_arm64.deb with a
desktop entry, app icon, and launcher. Bundles a relocatable venv (all Python
deps, no pip at install) and declares system deps (python3-gi, xdotool,
libnotify-bin, recorder). Built on /usr/bin/python3 so the tray works out of
the box. Installs via the Software app or `apt install ./…deb`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Minimal flat design with the Ubuntu font: clickable workflow rows with hover
(click to record/stop), muted descriptions and hotkey hints, subtle dividers,
and text-style Settings/Quit. Drops monogram avatars and per-row buttons.
Settings window restyled to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The upstream app is macOS-only (Swift/SwiftUI, CoreML/WhisperKit) and can't
run on Linux or in a container. This adds a native host tool under linux/ that
reproduces the workflow: focus any text field, press a hotkey, speak, and the
optionally-rewritten text is typed into that field.
- Engine: pynput global hotkeys → mic record → local faster-whisper →
optional OpenAI-compatible rewrite → xdotool typing into the focused window
- Frontends: system tray (AppIndicator, default), tkinter control panel,
and headless modes
- Config-driven workflows in ~/.config/blitztext/config.toml with per-workflow
prompt/model/temperature overrides
- Packaging: install.sh, requirements.txt, systemd user unit
- Targets X11; local transcription runs CPU int8 on this arm64 host
See linux/CHANGELOG.md and linux/README.md for details.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>