diff --git a/gui/log_viewer.py b/gui/log_viewer.py index 9b487af..5deca3d 100644 --- a/gui/log_viewer.py +++ b/gui/log_viewer.py @@ -6,7 +6,7 @@ from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QTextEdit, QLabel, QSizePolicy ) from PyQt5.QtCore import Qt, pyqtSignal, QTimer -from PyQt5.QtGui import QTextCursor, QFont +from PyQt5.QtGui import QTextCursor, QFont, QTextCharFormat from pathlib import Path import datetime import html @@ -120,6 +120,9 @@ class LogViewer(QWidget): def clear_log(self): """Clear the log display and reset tracking.""" self.log_display.clear() + # Reset the char format so the red color from previous [MISMATCH] + # HTML spans doesn't bleed into new plain-text appends. + self.log_display.setCurrentCharFormat(QTextCharFormat()) self.status_label.setText("Log cleared") # Reset tail file pointer and buffer if active self._tail_buffer = "" @@ -171,11 +174,26 @@ class LogViewer(QWidget): self._tail_timer.start(self._tail_interval) def stop_tail(self): - """Stop tailing the log file and close internal file handle.""" + """Stop tailing the log file and close internal file handle. + + Performs a final drain so no data written before this call is lost. + """ try: self._tail_timer.stop() except Exception: pass + # Final drain: read any remaining data before closing the handle + try: + self._poll_tail() + except Exception: + pass + # Flush any leftover partial line sitting in the buffer + if self._tail_buffer and self._tail_buffer.strip(): + try: + self.append_log_message(self._tail_buffer) + except Exception: + pass + self._tail_buffer = "" if self._tail_f: try: self._tail_f.close() @@ -235,12 +253,13 @@ class LogViewer(QWidget): if "[MISMATCH]" in message: escaped = html.escape(message) self.log_display.append(f'{escaped}') - # Emit signal on the header line ("Failed after retries") so the - # counter increments once per mismatch event, not per line. - if "Failed after retries" in message: - self.mismatch_detected.emit() + # Counting is handled via stdout MISMATCH_EVENT markers in + # TranslationTab.append_log — the log viewer only handles display. else: - self.log_display.append(message) + # Explicitly wrap in a white span so Qt doesn't inherit red from + # a preceding [MISMATCH] HTML append. + escaped = html.escape(message) + self.log_display.append(f'{escaped}') # Scroll to bottom cursor = self.log_display.textCursor() diff --git a/gui/translation_tab.py b/gui/translation_tab.py index 4d2980a..f7504c0 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -814,8 +814,8 @@ class TranslationTab(QWidget): self.reset_view_button.setStyleSheet(icon_btn_style) self.open_translations_button.setStyleSheet(icon_btn_style) # Ensure emoji/icons are readable - self.reset_view_button.setFont(QFont('', 12)) - self.open_translations_button.setFont(QFont('', 12)) + self.reset_view_button.setFont(QFont('Segoe UI', 12)) + self.open_translations_button.setFont(QFont('Segoe UI', 12)) # Create the stop button here so it sits in the same row as the # back/open buttons. Use a compact icon style to match them but @@ -849,7 +849,7 @@ class TranslationTab(QWidget): self.stop_button.clicked.connect(self.stop_translation) self.stop_button.setStyleSheet(stop_button_style) # Slightly larger font for the emoji to make it visually clear - self.stop_button.setFont(QFont('', 14)) + self.stop_button.setFont(QFont('Segoe UI', 14)) self.stop_button.setVisible(False) # Place both buttons on the left and totals on the right @@ -1017,6 +1017,9 @@ class TranslationTab(QWidget): # Right side - translation history log viewer self.translation_log_viewer = LogViewer() + # Mismatch counting is driven by MISMATCH_EVENT stdout markers + # detected in append_log. The log_viewer signal is kept as a + # fallback for in-process mode (e.g. speaker-parse). self.translation_log_viewer.mismatch_detected.connect(self.on_mismatch_detected) # Allow both left and right widgets to expand vertically so the @@ -1743,6 +1746,11 @@ class TranslationTab(QWidget): self.translation_worker.start() def append_log(self, message): + # Detect mismatch markers emitted to stdout by translation.py. + # This is the primary, non-racy detection path for subprocess mode. + if isinstance(message, str) and message.startswith("MISMATCH_EVENT:"): + self.on_mismatch_detected() + return # marker is internal, not displayed try: pattern = r'^\W*(?P[^:]+):.*?\[Input:\s*(?P\d+)\].*?\[Output:\s*(?P\d+)\].*?\[Cost:\s*\$(?P[\d\.]+)\].*?\[(?P