diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35c28e5..7ffc7f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
- Added a feature that allows selecting multiple items in book handler (in right click menu) by @jborza in #31, that fixes #28
- Added dark theme support, allowing users to switch between light and dark themes in the settings.
- Added auto-accept system to the chapter options dialog in conversion process, allowing the dialog to auto-accept after a certain time if no action is taken.
+- Added new option: `Configure max lines in log window` that allows configuring the maximum number of lines to display in the log window.
- Improvements in documentation and code.
# v1.0.9
diff --git a/README.md b/README.md
index 41867df..95cf045 100644
--- a/README.md
+++ b/README.md
@@ -146,6 +146,7 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex
|---------|-------------|
| **Theme** | Change the application's theme using `System`, `Light`, or `Dark` options. |
| **Configure max words per subtitle** | Configures the maximum number of words per subtitle entry. |
+| **Configure max lines in log window** | Configures the maximum number of lines to display in the log window. |
| **Separate chapters audio format** | Configures the audio format for separate chapters as `wav`, `flac`, `mp3`, or `opus`. |
| **Create desktop shortcut** | Creates a shortcut on your desktop for easy access. |
| **Open config.json directory** | Opens the directory where the configuration file is stored. |
diff --git a/abogen/constants.py b/abogen/constants.py
index e20140f..a403302 100644
--- a/abogen/constants.py
+++ b/abogen/constants.py
@@ -8,6 +8,9 @@ PROGRAM_DESCRIPTION = (
GITHUB_URL = "https://github.com/denizsafak/abogen"
VERSION = get_version()
+# Settings
+CHAPTER_OPTIONS_COUNTDOWN = 30 # Countdown seconds for chapter options
+
# Language description mapping
LANGUAGE_DESCRIPTIONS = {
"a": "American English",
diff --git a/abogen/conversion.py b/abogen/conversion.py
index 1cbe1bc..da25dcf 100644
--- a/abogen/conversion.py
+++ b/abogen/conversion.py
@@ -9,15 +9,14 @@ from platformdirs import user_desktop_dir
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
import soundfile as sf
-from utils import clean_text, create_process
-from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS, COLORS
-from voice_formulas import get_new_voice
-import hf_tracker
+from abogen.utils import clean_text, create_process
+from abogen.constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS, COLORS, CHAPTER_OPTIONS_COUNTDOWN
+from abogen.voice_formulas import get_new_voice
+import abogen.hf_tracker as hf_tracker
import static_ffmpeg
import threading # for efficient waiting
import subprocess
-
def get_sample_voice_text(lang_code):
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
@@ -73,7 +72,7 @@ class ChapterOptionsDialog(QDialog):
layout.addWidget(self.merge_at_end_checkbox)
# Countdown label
- self.countdown_seconds = 30
+ self.countdown_seconds = CHAPTER_OPTIONS_COUNTDOWN
self.countdown_label = QLabel(f"Auto-accepting in {self.countdown_seconds} seconds...")
self.countdown_label.setStyleSheet(f"color: {COLORS['GREEN']};")
layout.addWidget(self.countdown_label)
diff --git a/abogen/gui.py b/abogen/gui.py
index 93209b6..b43ac07 100644
--- a/abogen/gui.py
+++ b/abogen/gui.py
@@ -261,7 +261,7 @@ class InputBox(QLabel):
self.window().char_count = 0
# embed icon at native size with word-wrap for the filename
self.setText(
- f'
{name}
Size: {format_num(size_str)}
Characters: {format_num(char_count)}'
+ f'
{name}
Size: {size_str}
Characters: {format_num(char_count)}'
)
# Set fixed width to force wrapping
self.setWordWrap(True)
@@ -609,6 +609,8 @@ class abogen(QWidget):
self.displayed_file_path = (
None # Add new variable to track the displayed file path
)
+ # Max log lines
+ self.log_window_max_lines = self.config.get("log_window_max_lines", 5000)
self.selected_chapters = set()
self.last_opened_book_path = None # Track the last opened book path
self.last_output_path = None
@@ -658,6 +660,9 @@ class abogen(QWidget):
self.queued_items = []
self.current_queue_index = 0
+ # Log lines
+ self._log_lines = []
+
self.initUI()
self.speed_slider.setValue(int(self.config.get("speed", 1.00) * 100))
self.update_speed_label()
@@ -1367,6 +1372,7 @@ class abogen(QWidget):
self.input_box.hide()
self.log_text.show()
self.log_text.clear()
+ self._log_lines = []
QApplication.processEvents()
def restore_input_box(self):
@@ -1387,35 +1393,60 @@ class abogen(QWidget):
self._update_log_main_thread(message)
def _update_log_main_thread(self, message):
- sb = self.log_text.verticalScrollBar()
- prev_val = sb.value()
- at_bottom = prev_val == sb.maximum()
- # save current text cursor to preserve selection
- old_cursor = self.log_text.textCursor()
- # prepare html
+ # Efficient log: keep only last 10 lines in a Python list
+ if not hasattr(self, '_log_lines'):
+ self._log_lines = []
+
+ # Prepare the new log line as HTML
if isinstance(message, tuple):
text, spec = message
color = "green" if spec is True else ("red" if spec is False else spec)
html = self.color_text(text, color)
else:
html = str(message).replace("\n", "
") + "
"
- # move cursor to end for insertion
- insert_cursor = self.log_text.textCursor()
- insert_cursor.movePosition(QTextCursor.End)
- self.log_text.setTextCursor(insert_cursor)
- self.log_text.insertHtml(html)
- # restore original cursor/selection
- self.log_text.setTextCursor(old_cursor)
- # restore scroll position
- sb.setValue(sb.maximum() if at_bottom else prev_val)
+
+ # Split incoming html into lines (preserving blank lines)
+ new_lines = html.split("
")
+ # Remove the last empty string if html ends with
+ if new_lines and new_lines[-1] == "":
+ new_lines = new_lines[:-1]
+
+ # Append new lines to the buffer
+ self._log_lines.extend(new_lines)
+ # Keep only the last log_window_max_lines lines
+ self._log_lines = self._log_lines[-self.log_window_max_lines:]
+
+ # Join and set as HTML
+ self.log_text.setHtml("
".join(self._log_lines))
+
+ # Scroll to bottom if needed
+ sb = self.log_text.verticalScrollBar()
+ sb.setValue(sb.maximum())
QApplication.processEvents()
+ def _get_queue_progress_format(self, value=None):
+ """Return the progress bar format string for queue mode."""
+ if hasattr(self, 'queued_items') and self.queued_items and hasattr(self, 'current_queue_index'):
+ N = self.current_queue_index + 1
+ M = len(self.queued_items)
+ percent = value if value is not None else self.progress_bar.value()
+ return f"{percent}% ({N}/{M})"
+ else:
+ percent = value if value is not None else self.progress_bar.value()
+ return f"{percent}%"
+
def update_progress(self, value, etr_str): # Add etr_str parameter
# Ensure progress doesn't exceed 99%
if value >= 100:
value = 99
self.progress_bar.setValue(value)
- self.progress_bar.setFormat("%p%") # Keep format as percentage only
+ # Show queue progress if in queue mode
+ if hasattr(self, 'queued_items') and self.queued_items and hasattr(self, 'current_queue_index'):
+ N = self.current_queue_index + 1
+ M = len(self.queued_items)
+ self.progress_bar.setFormat(f"{value}% ({N}/{M})")
+ else:
+ self.progress_bar.setFormat(f"{value}%")
self.etr_label.setText(
f"Estimated time remaining: {etr_str}"
) # Update ETR label
@@ -1442,7 +1473,7 @@ class abogen(QWidget):
self.btn_manage_queue.setStyleSheet("")
# Change main Start button to 'Start queue' if queue has items
if enabled:
- self.btn_start.setText("Start queue")
+ self.btn_start.setText(f"Start queue ({len(self.queued_items)})")
try:
self.btn_start.clicked.disconnect()
except Exception:
@@ -1537,6 +1568,11 @@ class abogen(QWidget):
def start_queue(self):
self.current_queue_index = 0 # Start from the first item
+ # Set progress bar to 0% (1/M) immediately
+ if self.queued_items:
+ self.progress_bar.setValue(0)
+ self.progress_bar.setFormat(f"0% (1/{len(self.queued_items)})")
+ self.progress_bar.show()
self.start_next_queued_item()
def start_next_queued_item(self):
@@ -1561,7 +1597,6 @@ class abogen(QWidget):
# Called after each conversion finishes
self.current_queue_index += 1
if self.current_queue_index < len(self.queued_items):
- self.go_back_ui()
self.start_next_queued_item()
else:
self.current_queue_index = 0 # Reset for next time
@@ -1604,7 +1639,13 @@ class abogen(QWidget):
self.is_converting = True
self.convert_input_box_to_log()
self.progress_bar.setValue(0)
- self.progress_bar.setFormat("%p%") # Reset format initially
+ # Show queue progress if in queue mode
+ if from_queue and hasattr(self, 'queued_items') and self.queued_items and hasattr(self, 'current_queue_index'):
+ N = self.current_queue_index + 1
+ M = len(self.queued_items)
+ self.progress_bar.setFormat(f"0% ({N}/{M})")
+ else:
+ self.progress_bar.setFormat("%p%") # Reset format initially
self.etr_label.hide() # Hide ETR label initially
self.controls_widget.hide()
self.queue_row_widget.hide() # Hide queue row when process starts
@@ -1767,11 +1808,20 @@ class abogen(QWidget):
if self.open_file_btn:
self.open_file_btn.setVisible(show_open_file_button)
- self.controls_widget.hide()
- self.finish_widget.show()
- self.log_text.moveCursor(QTextCursor.End)
- self.log_text.ensureCursorVisible()
- save_config(self.config)
+ # Only show finish_widget if queue is done ---
+ if self.current_queue_index + 1 >= len(self.queued_items) or not self.queued_items:
+ # Queue finished, show finish screen
+ self.controls_widget.hide()
+ self.finish_widget.show()
+ self.log_text.moveCursor(QTextCursor.End)
+ self.log_text.ensureCursorVisible()
+ save_config(self.config)
+ else:
+ # More items in queue: clear log and reload for next item
+ self.log_text.clear()
+ self._log_lines = []
+ QApplication.processEvents()
+
# Start new queued item, if we're using a queued conversion
self.queue_item_conversion_finished()
@@ -1801,6 +1851,8 @@ class abogen(QWidget):
self.enable_disable_queue_buttons()
self.restore_input_box()
self.input_box.clear_input() # Reset text and style
+ # Trigger the "Clear Queue" button (simulate user click)
+ self.btn_clear_queue.click()
except Exception as e:
self._show_error_message_box("Reset Error", f"Could not reset UI:\n{e}")
@@ -1810,6 +1862,8 @@ class abogen(QWidget):
self.queue_row_widget.show() # Show queue row on go back
self.progress_bar.hide()
self.restore_input_box()
+ self.log_text.clear()
+ self._log_lines = []
# Use displayed_file_path instead of selected_file for EPUBs or PDFs
display_path = (
@@ -2187,6 +2241,8 @@ class abogen(QWidget):
self.queue_row_widget.show() # Show queue row on cancel
self.finish_widget.hide()
self.restore_input_box()
+ self.log_text.clear()
+ self._log_lines = []
display_path = (
self.displayed_file_path
if self.displayed_file_path
@@ -2449,7 +2505,9 @@ class abogen(QWidget):
max_words_action.triggered.connect(self.set_max_subtitle_words)
menu.addAction(max_words_action)
-
+ max_lines_action = QAction("Configure max lines in log window", self)
+ max_lines_action.triggered.connect(self.set_max_log_lines)
+ menu.addAction(max_lines_action)
# Add separator
menu.addSeparator()
@@ -2947,6 +3005,29 @@ Categories=AudioVideo;Audio;Utility;
self, "Error", f"An error occurred while clearing temporary files:\n{e}"
)
+ def set_max_log_lines(self):
+ """Open a dialog to set the maximum lines in the log window."""
+ from PyQt5.QtWidgets import QInputDialog
+
+ value, ok = QInputDialog.getInt(
+ self,
+ "Max Lines in Log Window",
+ "Enter the maximum number of lines to display in the log window:",
+ self.log_window_max_lines,
+ 10, # min value
+ 999999999, # max value
+ 1, # step
+ )
+ if ok:
+ self.log_window_max_lines = value
+ self.config["log_window_max_lines"] = value
+ save_config(self.config)
+ QMessageBox.information(
+ self,
+ "Setting Saved",
+ f"Maximum lines in log window set to {value}.",
+ )
+
def set_max_subtitle_words(self):
"""Open a dialog to set the maximum words per subtitle"""
from PyQt5.QtWidgets import QInputDialog