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>
64 lines
2.2 KiB
Python
64 lines
2.2 KiB
Python
"""Microphone recording by shelling out to a system recorder.
|
|
|
|
Records 16 kHz mono WAV (what Whisper wants) using whichever recorder is
|
|
available — no Python audio bindings or device configuration required.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
# Ordered by preference. Each maps a recorder name to an argv builder.
|
|
_RECORDERS: dict[str, list[str]] = {
|
|
"pw-record": ["pw-record", "--rate", "16000", "--channels", "1", "--format", "s16"],
|
|
"parecord": ["parecord", "--rate=16000", "--channels=1", "--format=s16le", "--file-format=wav"],
|
|
"arecord": ["arecord", "-q", "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "wav"],
|
|
}
|
|
|
|
|
|
def detect_recorder(preference: str = "auto") -> str:
|
|
if preference != "auto":
|
|
if shutil.which(preference):
|
|
return preference
|
|
raise RuntimeError(f"Configured recorder '{preference}' not found on PATH.")
|
|
for name in _RECORDERS:
|
|
if shutil.which(name):
|
|
return name
|
|
raise RuntimeError("No recorder found (need one of: pw-record, parecord, arecord).")
|
|
|
|
|
|
class Recording:
|
|
"""A single in-progress recording. Start on construction, call stop()."""
|
|
|
|
def __init__(self, recorder: str):
|
|
self._tmp = Path(tempfile.mkstemp(prefix="blitztext-", suffix=".wav")[1])
|
|
argv = list(_RECORDERS[recorder]) + [str(self._tmp)]
|
|
# arecord writes to the file given as a positional arg; the others too.
|
|
self._proc = subprocess.Popen(
|
|
argv,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
def stop(self) -> Path:
|
|
"""Stop recording, finalize the WAV, and return its path."""
|
|
if self._proc.poll() is None:
|
|
# SIGINT lets pw-record/arecord flush the WAV header cleanly.
|
|
self._proc.send_signal(signal.SIGINT)
|
|
try:
|
|
self._proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
self._proc.terminate()
|
|
self._proc.wait(timeout=5)
|
|
return self._tmp
|
|
|
|
def discard(self) -> None:
|
|
if self._proc.poll() is None:
|
|
self._proc.terminate()
|
|
self._proc.wait(timeout=5)
|
|
self._tmp.unlink(missing_ok=True)
|