blitztext-app-linux/linux/blitztext/logbuffer.py
mARTin-B78 f5f65ffba9 fix: stability, WhisperX 404, log levels (v2.03.00)
- Stability: remove blocking TCP socket call from _collect() — was
  freezing the GTK main thread on Save when wakeword server unreachable;
  now checks async in background and logs result
- Thread safety: fix _ww_load() reading GTK widget from background thread;
  capture URI on main thread before spawning
- WhisperX 404: _transcribe_remote now detects non-standard paths
  (anything other than /v1) and uses the URL as the full endpoint,
  so http://host/transcribe works without /audio/transcriptions appended
- Log levels: logbuffer stores (ts, level, msg) tuples; log() accepts
  level= (DEBUG/INFO/WARNING/ERROR); Log tab gets a Level dropdown
  (Verbose/Info/Warning/Error) that filters displayed entries live

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:01:14 +02:00

85 lines
2.4 KiB
Python

"""In-memory log buffer shown in Settings → Log, mirrored to stderr.
Our own messages go through log(); library logs (faster-whisper, huggingface_hub)
are captured via a logging handler so model download/load progress is visible
instead of an opaque "Loading…".
Each entry stores (timestamp_str, level, message) so the UI can filter by level.
Levels: DEBUG, INFO, WARNING, ERROR (default: INFO)
"""
from __future__ import annotations
import logging
import sys
import threading
import time
from collections import deque
# Each entry: (time_str, level, message)
_ENTRIES: deque[tuple[str, str, str]] = deque(maxlen=2000)
_LOCK = threading.Lock()
_LEVEL_ORDER = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3}
def log(msg: str, *, echo: bool = True, level: str = "INFO") -> None:
level = level.upper()
ts = time.strftime("%H:%M:%S")
with _LOCK:
_ENTRIES.append((ts, level, msg))
if echo:
print(f"{ts} [{level}] {msg}", file=sys.stderr, flush=True)
def lines(min_level: str = "DEBUG") -> list[str]:
"""Return formatted lines at or above min_level."""
threshold = _LEVEL_ORDER.get(min_level.upper(), 0)
with _LOCK:
entries = list(_ENTRIES)
result = []
for ts, lvl, msg in entries:
if _LEVEL_ORDER.get(lvl, 1) >= threshold:
prefix = f"[{lvl}] " if lvl not in ("INFO",) else ""
result.append(f"{ts} {prefix}{msg}")
return result
def clear() -> None:
with _LOCK:
_ENTRIES.clear()
class _BufferHandler(logging.Handler):
_PY_TO_LEVEL = {
logging.DEBUG: "DEBUG",
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.ERROR: "ERROR",
logging.CRITICAL:"ERROR",
}
def emit(self, record: logging.LogRecord) -> None:
try:
lvl = self._PY_TO_LEVEL.get(record.levelno, "INFO")
log(self.format(record), echo=False, level=lvl)
except Exception: # noqa: BLE001 - never let logging break the app
pass
_installed = False
def install_logging() -> None:
"""Capture library log records into the buffer (once)."""
global _installed
if _installed:
return
_installed = True
handler = _BufferHandler()
handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
for name in ("faster_whisper", "huggingface_hub", "blitztext"):
lg = logging.getLogger(name)
lg.addHandler(handler)
lg.setLevel(logging.DEBUG)