From 0875fe7ac0cbc1ae7b157a0b250dcf09a6df1f63 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 9 Jun 2026 15:02:45 +0200 Subject: [PATCH] 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 --- linux/blitztext/daemon.py | 24 ++++++++++++++++++++++++ linux/blitztext/llm.py | 7 +++++++ 2 files changed, 31 insertions(+) diff --git a/linux/blitztext/daemon.py b/linux/blitztext/daemon.py index 51b3087..df1a4fa 100644 --- a/linux/blitztext/daemon.py +++ b/linux/blitztext/daemon.py @@ -510,6 +510,9 @@ class Daemon: return self._emit("busy", label, "Rewriting…") self._dnotify(f"⌛ {label}", "Rewriting…") + # Show the transcribed text immediately so the user sees what was heard. + if self.text_cb: + self.text_cb(f"📝 {text}") # Stream the rewrite into the overlay so you watch the model write # (the bubble updates token-by-token). The delivered text is still # the complete result, typed once the rewrite finishes. @@ -559,6 +562,27 @@ class Daemon: self._emit("error", label, str(exc)) self._dnotify("Rewrite failed", str(exc), "critical") log(f"ERROR ({label} rewrite): {exc}") + if self.text_cb: + self.text_cb(f"✗ {exc}") + return + except Exception as exc: # noqa: BLE001 - guard against any uncaught error + msg = f"LLM error: {exc}" + self._emit("error", label, msg) + log(f"ERROR ({label} rewrite unexpected): {exc}") + if self.text_cb: + self.text_cb(f"✗ {msg}") + return + + # Sanity-check: reject responses that are >80 % whitespace — + # a model that's cold-starting or misconfigured sometimes streams + # spaces or blank lines instead of real output. + non_ws = sum(1 for c in text if not c.isspace()) + if non_ws < max(1, len(text) * 0.20): + msg = "LLM returned mostly whitespace — discarded" + self._emit("error", label, msg) + log(f"ERROR ({label}): {msg} (len={len(text)})") + if self.text_cb: + self.text_cb(f"✗ {msg}") return if not text: diff --git a/linux/blitztext/llm.py b/linux/blitztext/llm.py index 3a25b4d..0cdc295 100644 --- a/linux/blitztext/llm.py +++ b/linux/blitztext/llm.py @@ -93,6 +93,13 @@ def chat( raise LLMError(f"Connection failed: {exc.reason}") from exc except (KeyError, IndexError, TypeError) as exc: raise LLMError(f"Unexpected response: {exc}") from exc + except (TimeoutError, OSError) as exc: + # socket.timeout (subclass of OSError / TimeoutError) fires when the server + # stops sending data mid-stream. Not wrapped in URLError — must be caught + # separately or it would propagate uncaught and kill the background thread. + raise LLMError(f"Request timed out or connection lost: {exc}") from exc + except Exception as exc: # noqa: BLE001 + raise LLMError(f"Unexpected error: {exc}") from exc content = (content or "").strip() if not content: