Added chapter options countdown and new option "Configure max lines in log window", improvements in code

This commit is contained in:
Deniz Şafak
2025-07-11 03:31:47 +03:00
parent 6ede337658
commit 9b88a1ba55
5 changed files with 117 additions and 32 deletions
+1
View File
@@ -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
+1
View File
@@ -146,6 +146,7 @@ Heres 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. |
+3
View File
@@ -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",
+5 -6
View File
@@ -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)
+101 -20
View File
@@ -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'<img src="data:image/png;base64,{img_data}"><br><span style="display: inline-block; max-width: 100%; word-break: break-all;"><b>{name}</b></span><br>Size: {format_num(size_str)}<br>Characters: {format_num(char_count)}'
f'<img src="data:image/png;base64,{img_data}"><br><span style="display: inline-block; max-width: 100%; word-break: break-all;"><b>{name}</b></span><br>Size: {size_str}<br>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", "<br>") + "<br>"
# 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("<br>")
# Remove the last empty string if html ends with <br>
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("<br>".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,6 +1639,12 @@ class abogen(QWidget):
self.is_converting = True
self.convert_input_box_to_log()
self.progress_bar.setValue(0)
# 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()
@@ -1767,11 +1808,20 @@ class abogen(QWidget):
if self.open_file_btn:
self.open_file_btn.setVisible(show_open_file_button)
# 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