blitztext-app-linux/linux/blitztext/rewrite.py
mARTin-B78 80da5c6d15 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

74 lines
2.0 KiB
Python

"""Optional LLM rewrite via an OpenAI-compatible chat endpoint.
Uses only the standard library (urllib) so the daemon needs no extra deps for
the rewrite step. Works against OpenAI or any local server that implements the
/chat/completions API (e.g. vLLM, llama-swap).
"""
from __future__ import annotations
import json
import urllib.error
import urllib.request
class RewriteError(RuntimeError):
pass
def rewrite(
text: str,
system_prompt: str,
*,
base_url: str,
api_key: str | None,
model: str,
temperature: float = 0.3,
timeout: int = 45,
) -> str:
if not api_key:
raise RewriteError(
"No API key for rewrite. Set the env var named by rewrite.api_key_env, "
"or use a local endpoint that ignores auth."
)
payload = json.dumps(
{
"model": model,
"temperature": temperature,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text},
],
}
).encode("utf-8")
req = urllib.request.Request(
f"{base_url}/chat/completions",
data=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", "replace")
raise RewriteError(f"HTTP {exc.code}: {detail[:300]}") from exc
except urllib.error.URLError as exc:
raise RewriteError(f"Connection failed: {exc.reason}") from exc
try:
content = body["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as exc:
raise RewriteError(f"Unexpected response: {str(body)[:300]}") from exc
content = (content or "").strip()
if not content:
raise RewriteError("Empty response from model.")
return content