Skip to content

Commit ea63f07

Browse files
authored
chore: harden startup environment and simplify worker paths (#41)
* test: cover startup display environment * fix(worker): kaputten API-Key-Pre-Check im TranscribeWorker entfernen Der Pre-Check referenzierte self.config, das im Worker nicht existiert. Bei fehlendem API-Key sah der Nutzer dadurch einen AttributeError statt des Env-Var-Hinweises. LLMService.rewrite() meldet den fehlenden Key bereits selbst korrekt; der doppelte Check entfällt ersatzlos. Regressionstest deckt den Fehlerpfad ab. * refactor: doppeltes Thread-Wiring und Completion-Call zusammenführen ComposeWindow: _start_worker und _start_worker_raw teilten 13 Zeilen identisches QThread-Wiring; jetzt gemeinsamer _launch_worker-Helper. LLMService: dampf_ablassen, text_improver, emoji_text und rewrite_raw wiederholten denselben Completion-Aufruf inkl. Leerantwort-Check; jetzt gemeinsamer _chat_completion-Helper. Verhalten unverändert.
1 parent 82c8ac7 commit ea63f07

12 files changed

Lines changed: 166 additions & 186 deletions

ROADMAP.md

Lines changed: 0 additions & 94 deletions
This file was deleted.

app/audio_recorder.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""AudioRecorder for BlitztextLinux.
22
3-
Startet und stoppt Audioaufnahmen via parec (PulseAudio/PipeWire).
4-
Extrahiert aus whisper-dictation scripts/dictate_toggle.py v0.2.19.
3+
Starts and stops audio recordings via parec (PulseAudio/PipeWire).
54
65
Design:
76
- Synchron (kein Thread): der aufrufende Thread blockiert nicht,
@@ -23,7 +22,7 @@
2322

2423
logger = logging.getLogger("blitztext.audio_recorder")
2524

26-
# Aufnahme-Parameter (identisch zu whisper-dictation)
25+
# Recording parameters for Whisper-compatible 16 kHz mono WAV input.
2726
_RATE = 16000
2827
_CHANNELS = 1
2928
_LATENCY_MS = 100
@@ -102,7 +101,7 @@ def start(self, device: str = "@DEFAULT_SOURCE@") -> Path:
102101
"parec nicht gefunden. Bitte installieren: sudo apt install pulseaudio-utils"
103102
)
104103

105-
# Stale-PID-Schutz (aus whisper-dictation v0.2.19)
104+
# Stale PID guard for interrupted recording processes.
106105
self._cleanup_stale_pid()
107106

108107
if self._pid_file.is_file():

app/blitztext_linux.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
sys.path.insert(0, PROJECT_DIR)
3232

3333
from app.config import Config, DEFAULTS, VALID_HOTKEY_KEYS
34-
from app.llm_service import LLMService, WorkflowType, LLM_WORKFLOWS, LLMServiceError
34+
from app.llm_service import LLMService, WorkflowType, LLM_WORKFLOWS
3535
from app.writing_presets import WRITING_PRESET_KEYS, get_preset, preset_index
3636
from app.hotkey_service import HotkeyWorker
3737
from app.audio_recorder import AudioRecorder, AudioRecorderError
@@ -587,13 +587,10 @@ def run(self) -> None:
587587
if not transcript or not transcript.strip():
588588
raise TranscribeError("Keine Sprache im Audio erkannt.")
589589

590-
# LLM rewrite if it is an LLM workflow
590+
# LLM rewrite if it is an LLM workflow. rewrite() meldet einen
591+
# fehlenden API-Key selbst als LLMServiceError mit Env-Var-Hinweis.
591592
if self.workflow in LLM_WORKFLOWS:
592593
self._emit("status_changed", "rewriting")
593-
if not self.llm_service.is_available():
594-
raise LLMServiceError(
595-
f"OpenAI API-Key nicht gesetzt. Bitte {self.config.openai_api_key_env} in ~/.config/blitztext-linux/secrets.env setzen."
596-
)
597594
result_text = self.llm_service.rewrite(self.workflow, transcript)
598595
else:
599596
result_text = transcript

app/compose_window.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,6 @@ def _start_worker(self, text: str) -> None:
655655
else:
656656
writing_preset = self._selected_preset()
657657

658-
thread = QThread(self)
659658
worker = _ComposeWorker(
660659
self._llm_service,
661660
workflow,
@@ -664,6 +663,11 @@ def _start_worker(self, text: str) -> None:
664663
tone=tone,
665664
custom_prompt=custom_prompt,
666665
)
666+
self._launch_worker(worker)
667+
668+
def _launch_worker(self, worker: _ComposeWorker) -> None:
669+
"""Gemeinsames Thread-Wiring für Standard- und Raw-Prompt-Worker."""
670+
thread = QThread(self)
667671
worker.moveToThread(thread)
668672
thread.started.connect(worker.run)
669673
worker.finished.connect(self._on_worker_result)
@@ -739,7 +743,6 @@ def _on_show_prompt_clicked(self) -> None:
739743
self._start_worker_raw(dialog.get_system_prompt(), dialog.get_user_message())
740744

741745
def _start_worker_raw(self, system_prompt: str, user_message: str) -> None:
742-
thread = QThread(self)
743746
worker = _ComposeWorker(
744747
self._llm_service,
745748
self._selected_workflow(),
@@ -748,19 +751,7 @@ def _start_worker_raw(self, system_prompt: str, user_message: str) -> None:
748751
raw_system_prompt=system_prompt,
749752
raw_user_message=user_message,
750753
)
751-
worker.moveToThread(thread)
752-
thread.started.connect(worker.run)
753-
worker.finished.connect(self._on_worker_result)
754-
worker.error.connect(self._on_worker_error)
755-
worker.finished.connect(thread.quit)
756-
worker.error.connect(thread.quit)
757-
thread.finished.connect(worker.deleteLater)
758-
thread.finished.connect(thread.deleteLater)
759-
thread.finished.connect(self._on_worker_thread_finished)
760-
self._worker_thread = thread
761-
self._worker = worker
762-
self._set_busy(True)
763-
thread.start()
754+
self._launch_worker(worker)
764755

765756
@pyqtSlot(str)
766757
def _on_worker_result(self, result_text: str) -> None:

app/history_panel.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
"""Verlauf-/Diktat-Panel fuer BlitztextLinux.
2-
3-
Portiert und an die monolithische Blitztext-Architektur angepasst aus
4-
whisper-dictation app/gui/history_panel.py.
1+
"""History window for Blitztext Linux.
52
63
GUI-freie Logik (Notiz speichern, Diktat zusammenfuehren) liegt in
74
Modulfunktionen, damit sie ohne Qt/Display testbar ist.

app/llm_service.py

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -176,18 +176,13 @@ def _rewrite_for_workflow(
176176
raise
177177
raise LLMServiceError(f"OpenAI API-Fehler: {exc}") from exc
178178

179-
def dampf_ablassen(self, transcript: str, custom_system_prompt: str = "") -> str:
180-
self._check_openai()
181-
if not transcript or not transcript.strip():
182-
raise ValueError("transcript must not be empty")
183-
184-
system = (custom_system_prompt.strip() or self.dampf_system_prompt.strip() or _DAMPF_SYSTEM) + self._custom_terms_instruction()
185-
179+
def _chat_completion(self, system: str, user: str) -> str:
180+
"""Führt den Chat-Completion-Aufruf aus; gemeinsamer Pfad aller Workflows."""
186181
response = self.client.chat.completions.create(
187182
model=self.model,
188183
messages=[
189184
{"role": "system", "content": system},
190-
{"role": "user", "content": transcript.strip()},
185+
{"role": "user", "content": user.strip()},
191186
],
192187
temperature=0.7,
193188
)
@@ -196,6 +191,14 @@ def dampf_ablassen(self, transcript: str, custom_system_prompt: str = "") -> str
196191
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
197192
return content.strip()
198193

194+
def dampf_ablassen(self, transcript: str, custom_system_prompt: str = "") -> str:
195+
self._check_openai()
196+
if not transcript or not transcript.strip():
197+
raise ValueError("transcript must not be empty")
198+
199+
system = (custom_system_prompt.strip() or self.dampf_system_prompt.strip() or _DAMPF_SYSTEM) + self._custom_terms_instruction()
200+
return self._chat_completion(system, transcript)
201+
199202
def text_improver(self, transcript: str, tone: str = "neutral", custom_prompt: str = "") -> str:
200203
self._check_openai()
201204
if not transcript or not transcript.strip():
@@ -204,19 +207,7 @@ def text_improver(self, transcript: str, tone: str = "neutral", custom_prompt: s
204207
raise ValueError(f"invalid tone: {tone}")
205208

206209
system = (custom_prompt.strip() or _TEXT_IMPROVER_SYSTEM_TEMPLATE.format(tone=tone)) + self._custom_terms_instruction()
207-
208-
response = self.client.chat.completions.create(
209-
model=self.model,
210-
messages=[
211-
{"role": "system", "content": system},
212-
{"role": "user", "content": transcript.strip()},
213-
],
214-
temperature=0.7,
215-
)
216-
content = response.choices[0].message.content
217-
if content is None:
218-
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
219-
return content.strip()
210+
return self._chat_completion(system, transcript)
220211

221212
def emoji_text(self, transcript: str, density: str = "mittel") -> str:
222213
self._check_openai()
@@ -226,19 +217,7 @@ def emoji_text(self, transcript: str, density: str = "mittel") -> str:
226217
raise ValueError(f"invalid density: {density}")
227218

228219
system = _EMOJI_SYSTEM_TEMPLATE.format(density=density) + self._custom_terms_instruction()
229-
230-
response = self.client.chat.completions.create(
231-
model=self.model,
232-
messages=[
233-
{"role": "system", "content": system},
234-
{"role": "user", "content": transcript.strip()},
235-
],
236-
temperature=0.7,
237-
)
238-
content = response.choices[0].message.content
239-
if content is None:
240-
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
241-
return content.strip()
220+
return self._chat_completion(system, transcript)
242221

243222
def rewrite(self, workflow: WorkflowType, transcript: str) -> str:
244223
"""Send transcript to OpenAI and return the rewritten text.
@@ -306,18 +285,7 @@ def rewrite_raw(self, system_prompt: str, user_message: str) -> str:
306285
if not user_message or not user_message.strip():
307286
raise ValueError("user_message must not be empty")
308287
try:
309-
response = self.client.chat.completions.create(
310-
model=self.model,
311-
messages=[
312-
{"role": "system", "content": system_prompt},
313-
{"role": "user", "content": user_message.strip()},
314-
],
315-
temperature=0.7,
316-
)
317-
content = response.choices[0].message.content
318-
if content is None:
319-
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
320-
return content.strip()
288+
return self._chat_completion(system_prompt, user_message)
321289
except Exception as exc:
322290
if isinstance(exc, LLMServiceError):
323291
raise

app/notify.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
"""Desktop-Benachrichtigungen via notify-send.
1+
"""Desktop notifications for BlitztextLinux.
22
3-
Portiert aus whisper-dictation app/gui/notify.py. Schlaegt nie hart fehl --
4-
fehlt notify-send, wird die Meldung still uebersprungen.
3+
Notifications never fail hard. If notify-send is unavailable, the message is
4+
silently skipped.
55
"""
66
from __future__ import annotations
77

app/paste_service.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""PasteService for BlitztextLinux.
22
3-
Kopiert/extrahiert aus whisper-dictation scripts/dictate_toggle.py v0.2.19.
4-
53
Zwei Schritte:
64
1. wl-copy/xclip -- Text in Clipboard schreiben
75
2. ydotool -- Ctrl+V simulieren (nur wenn autopaste=True)
@@ -20,9 +18,9 @@
2018

2119
logger = logging.getLogger("blitztext.paste_service")
2220

23-
# Verzoegerung zwischen wl-copy und ydotool key (identisch zu whisper-dictation)
21+
# Delay between clipboard write and simulated paste key.
2422
_PASTE_DELAY = 0.15
25-
# ydotool key-delay in ms (identisch zu whisper-dictation)
23+
# ydotool key-delay in ms.
2624
_KEY_DELAY_MS = 80
2725
# Strg+V als rohe Keycodes (`<keycode>:<pressed>`). ydotool >=1.0
2826
# interpretiert KEINE Tastennamen mehr wie "ctrl+v" -- solche Werte werden

app/transcribe.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
#!/usr/bin/env python3
22
"""Transcribe a WAV file with Whisper.
33
4-
Kopiert aus whisper-dictation app/transcribe.py v0.2.19.
5-
Aenderungen gegenueber Original:
6-
- Logger-Name: blitztext.transcribe
7-
- Neue oeffentliche Funktion transcribe() fuer direkten Python-Import
8-
(BlitztextLinux ruft nicht per subprocess auf)
9-
- main() und CLI-Interface bleiben unveraendert erhalten
10-
114
Supports both openai-whisper and faster-whisper backends.
125
"""
136
from __future__ import annotations
@@ -33,7 +26,7 @@ class TranscribeError(Exception):
3326

3427

3528
# ---------------------------------------------------------------------------
36-
# Public API (neu gegenueber whisper-dictation)
29+
# Public API
3730
# ---------------------------------------------------------------------------
3831

3932
def transcribe(
@@ -87,7 +80,7 @@ def transcribe(
8780

8881

8982
# ---------------------------------------------------------------------------
90-
# Backend-Implementierungen (unveraendert aus whisper-dictation)
83+
# Backend implementations
9184
# ---------------------------------------------------------------------------
9285

9386
def _normalize_language(language: str) -> Optional[str]:
@@ -203,7 +196,7 @@ def _transcribe_faster(
203196

204197

205198
# ---------------------------------------------------------------------------
206-
# CLI (unveraendert aus whisper-dictation)
199+
# CLI
207200
# ---------------------------------------------------------------------------
208201

209202
def main() -> int:

docs/setup.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ systemctl --user disable blitztext-linux
3737
```
3838
</details>
3939

40+
## Desktop session notes
41+
42+
BlitztextLinux is developed for KDE Plasma on Wayland, with X11 fallbacks where the
43+
underlying tools support them.
44+
45+
- GUI startup needs a real desktop session: either a usable `WAYLAND_DISPLAY` socket
46+
or `DISPLAY` must be available. In headless shells, `scripts/verify.sh` can report
47+
a warning even when the installed dependencies are otherwise correct.
48+
- Qt prefers Wayland when `WAYLAND_DISPLAY` points to an existing socket. If that
49+
variable is stale but `DISPLAY` is set, the launcher falls back to X11.
50+
- Clipboard support uses `wl-copy`/`wl-paste` on Wayland and `xclip` on X11.
51+
- Auto-paste uses `ydotool`; terminal windows may need `Ctrl+Shift+V` instead of
52+
`Ctrl+V`, so the app detects known terminal window classes when possible.
53+
- Global hotkeys still use `evdev`/the `input` group. A future desktop-native XDG
54+
GlobalShortcuts integration would be a larger replacement, not a small setup fix.
55+
4056
## Manual install
4157

4258
If you want to debug the Linux setup path step by step:

0 commit comments

Comments
 (0)