From d487996281f71ccd3034dd708fa099057e528b42 Mon Sep 17 00:00:00 2001 From: historia Date: Fri, 28 Aug 2026 03:16:14 -0400 Subject: feat: unified logging with logging_kit.py --- app/ui/runview.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) (limited to 'app/ui/runview.py') diff --git a/app/ui/runview.py b/app/ui/runview.py index 9448c06..b3a1ab0 100644 --- a/app/ui/runview.py +++ b/app/ui/runview.py @@ -29,7 +29,6 @@ so the failure is never scrolled away. """ import contextlib -import io import threading import time from dataclasses import dataclass @@ -59,6 +58,51 @@ _SERVER_STATES = { _MONITOR_INTERVAL = 2.0 +class _LogAppender: + """A file-like that appends redirected console output to the run's log. + + The run view owns the screen, so anything a conversion prints to + stdout/stderr outside the progress events would otherwise be swallowed + silently; this mirrors it line by line into the run's dated log file + (RunConfig.log_path, the audiobook_ day stream), prefixed with the same + timestamp format the converter's log records use. Best-effort: write + errors are swallowed, and an empty path disables logging. + """ + + def __init__(self, path: str): + self._path = path + self._buffer = "" + + def write(self, text: str) -> int: + if not text: + return 0 + self._buffer += text + while True: + cut = self._buffer.find("\n") + if cut < 0: + break + line, self._buffer = self._buffer[:cut], self._buffer[cut + 1:] + self._append(line) + return len(text) + + def flush(self) -> None: + if self._buffer: + self._append(self._buffer) + self._buffer = "" + + def isatty(self) -> bool: + return False + + def _append(self, line: str) -> None: + if not self._path or not line.strip(): + return + try: + with open(self._path, "a", encoding="utf-8") as logf: + logf.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} - {line}\n") + except OSError: + pass + + @dataclass class RunConfig: """Everything the run view needs to execute one conversion. @@ -232,7 +276,7 @@ class RunView(ScreenView): import audiobook config = self.config try: - with contextlib.redirect_stdout(io.StringIO()): + with contextlib.redirect_stdout(_LogAppender(config.log_path)): if config.autostart_spec is not None: if config.restart_first: # The managed qwen server hosts another model than @@ -401,7 +445,8 @@ class RunView(ScreenView): def _stop() -> None: try: - with contextlib.redirect_stdout(io.StringIO()): + with contextlib.redirect_stdout( + _LogAppender(self.config.log_path)): servers.stop(name) finally: self._queue.put({"kind": "server_stopped"}) -- cgit v1.2.3