Commit Graph

30 Commits

Author SHA1 Message Date
0d1311ba18 stability: fix GTK thread safety, wakeword hang, freeze diagnostic (v2.0.0)
- 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>
2026-06-09 15:29:45 +02:00
0875fe7ac0 llm: catch all exceptions, validate response, show transcription preview (v1.9.9)
- 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>
2026-06-09 15:02:45 +02:00
749ef78383 dictation: spoken newlines + LLM thinking animation (v1.9.8)
- 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>
2026-06-09 14:43:15 +02:00
946d344090 presets: per-preset LLM engine selector + fix streaming freeze (v1.9.7)
- 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>
2026-06-09 14:39:59 +02:00
28020fdeda routing: send-by-voice keyword (types + presses Enter); wakeword benchmark via TTS; Release 1.8.0
- 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>
2026-06-08 13:17:33 +02:00
1c64f19bce overlay: drive waveform + silence countdown via pw-record (fix PipeWire); app reports as "Blitztext" not __main__.py; Release 1.7.1
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>
2026-06-08 08:39:37 +02:00
1567202e1a routing: spoken cancel keyword to discard a clip; Release 1.7.0
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>
2026-06-07 22:09:07 +02:00
ae01472b77 overlay: fix AT-SPI caret freeze; fuse routing match + live LLM into HUD; Release 1.6.0
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>
2026-06-07 16:58:47 +02:00
6a9c1fc95a overlay: silence auto-stop countdown ring around the mic; Release 1.5.1
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>
2026-06-07 15:41:17 +02:00
d55cdafcc1 on-screen dictation overlay at the cursor; Release 1.5.0
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>
2026-06-07 13:13:41 +02:00
227e4407c4 routing: announce which preset/keyword you triggered (incl. hands-free)
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>
2026-06-07 10:43:08 +02:00
ce0079e728 wakeword: independent cues + configurable auto-stop silence; add MANUAL.md
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>
2026-06-07 08:36:35 +02:00
436076620c audio cues: add master on/off switch; silence PortAudio teardown noise
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>
2026-06-06 23:45:47 +02:00
080e7297dd wakeword: stop hands-free notifications leaking when away; surface paused state
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>
2026-06-06 23:10:48 +02:00
8e1419e828 Add wakeword-specific audio cues (detected / captured)
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>
2026-06-06 14:34:49 +02:00
87eee3f103 Silence notifications for wakeword-triggered (hands-free) sessions
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>
2026-06-06 11:09:04 +02:00
6448711f65 Stop notification storms: transient notifications, single instance, no empty-result alerts
- 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>
2026-06-06 10:42:13 +02:00
209b4ef259 Add custom WAV audio cues for recording start/stop
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>
2026-06-06 09:40:09 +02:00
47a5f409cf Fix shutil import and add Drag-and-Drop UI reordering 2026-06-05 19:26:27 +02:00
953bf9f287 Add audio feedback (beeps) on recording start/stop 2026-06-05 18:22:47 +02:00
7e0ef9c92f Add VAD auto-stop after 2.5s of silence during dictation 2026-06-05 18:16:55 +02:00
5484bc6a1b Add Wayland, Tests, CI, and OpenWakeword integration
- 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.
2026-06-05 17:16:05 +02:00
457835929e Add realtime streaming and Linux app docs 2026-06-05 15:06:17 +02:00
78350bdb99 Add Log tab + in-memory log buffer
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>
2026-06-05 09:42:48 +02:00
44ca0feabd Redesign Settings: preset/engine managers, mic + level meter, autostart
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>
2026-06-04 23:13:36 +02:00
cf7ed562f5 Add transcription quality gate (silence + hallucination rejection)
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>
2026-06-04 23:02:55 +02:00
a104f2cfc4 Add modifier-key input scheme (whisper-key style)
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>
2026-06-04 20:26:59 +02:00
3026f1ff90 Add STT + LLM engine presets, rename entry to blitztext.py, macOS app icon
- 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>
2026-06-04 20:21:03 +02:00
34e4736080 Add voice-keyword routing
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>
2026-06-04 09:47:12 +02:00
83d4143e83 Add Blitztext for Linux v1.0.0 — native dictation tool
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>
2026-06-03 22:44:28 +02:00