Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 0 additions & 94 deletions ROADMAP.md

This file was deleted.

7 changes: 3 additions & 4 deletions app/audio_recorder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""AudioRecorder for BlitztextLinux.

Startet und stoppt Audioaufnahmen via parec (PulseAudio/PipeWire).
Extrahiert aus whisper-dictation scripts/dictate_toggle.py v0.2.19.
Starts and stops audio recordings via parec (PulseAudio/PipeWire).

Design:
- Synchron (kein Thread): der aufrufende Thread blockiert nicht,
Expand All @@ -23,7 +22,7 @@

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

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

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

if self._pid_file.is_file():
Expand Down
9 changes: 3 additions & 6 deletions app/blitztext_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
sys.path.insert(0, PROJECT_DIR)

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

# LLM rewrite if it is an LLM workflow
# LLM rewrite if it is an LLM workflow. rewrite() meldet einen
# fehlenden API-Key selbst als LLMServiceError mit Env-Var-Hinweis.
if self.workflow in LLM_WORKFLOWS:
self._emit("status_changed", "rewriting")
if not self.llm_service.is_available():
raise LLMServiceError(
f"OpenAI API-Key nicht gesetzt. Bitte {self.config.openai_api_key_env} in ~/.config/blitztext-linux/secrets.env setzen."
)
result_text = self.llm_service.rewrite(self.workflow, transcript)
else:
result_text = transcript
Expand Down
21 changes: 6 additions & 15 deletions app/compose_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,6 @@ def _start_worker(self, text: str) -> None:
else:
writing_preset = self._selected_preset()

thread = QThread(self)
worker = _ComposeWorker(
self._llm_service,
workflow,
Expand All @@ -664,6 +663,11 @@ def _start_worker(self, text: str) -> None:
tone=tone,
custom_prompt=custom_prompt,
)
self._launch_worker(worker)

def _launch_worker(self, worker: _ComposeWorker) -> None:
"""Gemeinsames Thread-Wiring für Standard- und Raw-Prompt-Worker."""
thread = QThread(self)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(self._on_worker_result)
Expand Down Expand Up @@ -739,7 +743,6 @@ def _on_show_prompt_clicked(self) -> None:
self._start_worker_raw(dialog.get_system_prompt(), dialog.get_user_message())

def _start_worker_raw(self, system_prompt: str, user_message: str) -> None:
thread = QThread(self)
worker = _ComposeWorker(
self._llm_service,
self._selected_workflow(),
Expand All @@ -748,19 +751,7 @@ def _start_worker_raw(self, system_prompt: str, user_message: str) -> None:
raw_system_prompt=system_prompt,
raw_user_message=user_message,
)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(self._on_worker_result)
worker.error.connect(self._on_worker_error)
worker.finished.connect(thread.quit)
worker.error.connect(thread.quit)
thread.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(self._on_worker_thread_finished)
self._worker_thread = thread
self._worker = worker
self._set_busy(True)
thread.start()
self._launch_worker(worker)

@pyqtSlot(str)
def _on_worker_result(self, result_text: str) -> None:
Expand Down
5 changes: 1 addition & 4 deletions app/history_panel.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
"""Verlauf-/Diktat-Panel fuer BlitztextLinux.

Portiert und an die monolithische Blitztext-Architektur angepasst aus
whisper-dictation app/gui/history_panel.py.
"""History window for Blitztext Linux.

GUI-freie Logik (Notiz speichern, Diktat zusammenfuehren) liegt in
Modulfunktionen, damit sie ohne Qt/Display testbar ist.
Expand Down
60 changes: 14 additions & 46 deletions app/llm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,13 @@ def _rewrite_for_workflow(
raise
raise LLMServiceError(f"OpenAI API-Fehler: {exc}") from exc

def dampf_ablassen(self, transcript: str, custom_system_prompt: str = "") -> str:
self._check_openai()
if not transcript or not transcript.strip():
raise ValueError("transcript must not be empty")

system = (custom_system_prompt.strip() or self.dampf_system_prompt.strip() or _DAMPF_SYSTEM) + self._custom_terms_instruction()

def _chat_completion(self, system: str, user: str) -> str:
"""Führt den Chat-Completion-Aufruf aus; gemeinsamer Pfad aller Workflows."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": transcript.strip()},
{"role": "user", "content": user.strip()},
],
temperature=0.7,
)
Expand All @@ -196,6 +191,14 @@ def dampf_ablassen(self, transcript: str, custom_system_prompt: str = "") -> str
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
return content.strip()

def dampf_ablassen(self, transcript: str, custom_system_prompt: str = "") -> str:
self._check_openai()
if not transcript or not transcript.strip():
raise ValueError("transcript must not be empty")

system = (custom_system_prompt.strip() or self.dampf_system_prompt.strip() or _DAMPF_SYSTEM) + self._custom_terms_instruction()
return self._chat_completion(system, transcript)

def text_improver(self, transcript: str, tone: str = "neutral", custom_prompt: str = "") -> str:
self._check_openai()
if not transcript or not transcript.strip():
Expand All @@ -204,19 +207,7 @@ def text_improver(self, transcript: str, tone: str = "neutral", custom_prompt: s
raise ValueError(f"invalid tone: {tone}")

system = (custom_prompt.strip() or _TEXT_IMPROVER_SYSTEM_TEMPLATE.format(tone=tone)) + self._custom_terms_instruction()

response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": transcript.strip()},
],
temperature=0.7,
)
content = response.choices[0].message.content
if content is None:
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
return content.strip()
return self._chat_completion(system, transcript)

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

system = _EMOJI_SYSTEM_TEMPLATE.format(density=density) + self._custom_terms_instruction()

response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": transcript.strip()},
],
temperature=0.7,
)
content = response.choices[0].message.content
if content is None:
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
return content.strip()
return self._chat_completion(system, transcript)

def rewrite(self, workflow: WorkflowType, transcript: str) -> str:
"""Send transcript to OpenAI and return the rewritten text.
Expand Down Expand Up @@ -306,18 +285,7 @@ def rewrite_raw(self, system_prompt: str, user_message: str) -> str:
if not user_message or not user_message.strip():
raise ValueError("user_message must not be empty")
try:
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message.strip()},
],
temperature=0.7,
)
content = response.choices[0].message.content
if content is None:
raise LLMServiceError("OpenAI hat eine leere Antwort zurückgegeben.")
return content.strip()
return self._chat_completion(system_prompt, user_message)
except Exception as exc:
if isinstance(exc, LLMServiceError):
raise
Expand Down
6 changes: 3 additions & 3 deletions app/notify.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Desktop-Benachrichtigungen via notify-send.
"""Desktop notifications for BlitztextLinux.

Portiert aus whisper-dictation app/gui/notify.py. Schlaegt nie hart fehl --
fehlt notify-send, wird die Meldung still uebersprungen.
Notifications never fail hard. If notify-send is unavailable, the message is
silently skipped.
"""
from __future__ import annotations

Expand Down
6 changes: 2 additions & 4 deletions app/paste_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""PasteService for BlitztextLinux.

Kopiert/extrahiert aus whisper-dictation scripts/dictate_toggle.py v0.2.19.

Zwei Schritte:
1. wl-copy/xclip -- Text in Clipboard schreiben
2. ydotool -- Ctrl+V simulieren (nur wenn autopaste=True)
Expand All @@ -20,9 +18,9 @@

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

# Verzoegerung zwischen wl-copy und ydotool key (identisch zu whisper-dictation)
# Delay between clipboard write and simulated paste key.
_PASTE_DELAY = 0.15
# ydotool key-delay in ms (identisch zu whisper-dictation)
# ydotool key-delay in ms.
_KEY_DELAY_MS = 80
# Strg+V als rohe Keycodes (`<keycode>:<pressed>`). ydotool >=1.0
# interpretiert KEINE Tastennamen mehr wie "ctrl+v" -- solche Werte werden
Expand Down
13 changes: 3 additions & 10 deletions app/transcribe.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
#!/usr/bin/env python3
"""Transcribe a WAV file with Whisper.

Kopiert aus whisper-dictation app/transcribe.py v0.2.19.
Aenderungen gegenueber Original:
- Logger-Name: blitztext.transcribe
- Neue oeffentliche Funktion transcribe() fuer direkten Python-Import
(BlitztextLinux ruft nicht per subprocess auf)
- main() und CLI-Interface bleiben unveraendert erhalten

Supports both openai-whisper and faster-whisper backends.
"""
from __future__ import annotations
Expand All @@ -33,7 +26,7 @@ class TranscribeError(Exception):


# ---------------------------------------------------------------------------
# Public API (neu gegenueber whisper-dictation)
# Public API
# ---------------------------------------------------------------------------

def transcribe(
Expand Down Expand Up @@ -87,7 +80,7 @@ def transcribe(


# ---------------------------------------------------------------------------
# Backend-Implementierungen (unveraendert aus whisper-dictation)
# Backend implementations
# ---------------------------------------------------------------------------

def _normalize_language(language: str) -> Optional[str]:
Expand Down Expand Up @@ -203,7 +196,7 @@ def _transcribe_faster(


# ---------------------------------------------------------------------------
# CLI (unveraendert aus whisper-dictation)
# CLI
# ---------------------------------------------------------------------------

def main() -> int:
Expand Down
16 changes: 16 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ systemctl --user disable blitztext-linux
```
</details>

## Desktop session notes

BlitztextLinux is developed for KDE Plasma on Wayland, with X11 fallbacks where the
underlying tools support them.

- GUI startup needs a real desktop session: either a usable `WAYLAND_DISPLAY` socket
or `DISPLAY` must be available. In headless shells, `scripts/verify.sh` can report
a warning even when the installed dependencies are otherwise correct.
- Qt prefers Wayland when `WAYLAND_DISPLAY` points to an existing socket. If that
variable is stale but `DISPLAY` is set, the launcher falls back to X11.
- Clipboard support uses `wl-copy`/`wl-paste` on Wayland and `xclip` on X11.
- Auto-paste uses `ydotool`; terminal windows may need `Ctrl+Shift+V` instead of
`Ctrl+V`, so the app detects known terminal window classes when possible.
- Global hotkeys still use `evdev`/the `input` group. A future desktop-native XDG
GlobalShortcuts integration would be a larger replacement, not a small setup fix.

## Manual install

If you want to debug the Linux setup path step by step:
Expand Down
Loading