blitztext-app-linux/linux/blitztext/transcribe.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

60 lines
2.0 KiB
Python

"""Local speech-to-text via faster-whisper (CTranslate2).
The model is loaded once and reused. On this arm64 host the CTranslate2 wheel is
CPU-only, so device="auto" attempts CUDA and falls back to CPU automatically.
"""
from __future__ import annotations
import sys
from pathlib import Path
class Transcriber:
def __init__(
self,
model: str = "small",
device: str = "auto",
compute_type: str = "auto",
beam_size: int = 5,
):
self.beam_size = beam_size
self._model = self._load(model, device, compute_type)
@staticmethod
def _resolve_compute_type(device: str, requested: str) -> str:
if requested != "auto":
return requested
return "float16" if device == "cuda" else "int8"
def _load(self, model: str, device: str, compute_type: str):
from faster_whisper import WhisperModel
attempts: list[str]
if device == "auto":
attempts = ["cuda", "cpu"]
else:
attempts = [device]
last_err: Exception | None = None
for dev in attempts:
try:
ct = self._resolve_compute_type(dev, compute_type)
m = WhisperModel(model, device=dev, compute_type=ct)
print(f"[blitztext] Whisper '{model}' loaded on {dev} ({ct})", file=sys.stderr)
return m
except Exception as exc: # noqa: BLE001 - CUDA libs may be absent
last_err = exc
if dev != attempts[-1]:
print(f"[blitztext] {dev} unavailable ({exc}); trying next device", file=sys.stderr)
raise RuntimeError(f"Failed to load Whisper model '{model}': {last_err}")
def transcribe(self, audio_path: Path, language: str = "") -> str:
segments, _info = self._model.transcribe(
str(audio_path),
language=language or None,
beam_size=self.beam_size,
vad_filter=True,
)
return " ".join(seg.text.strip() for seg in segments).strip()