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