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 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 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 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. - Improvements in documentation and code.
# v1.0.9 # 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. | | **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 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`. | | **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. | | **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. | | **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" GITHUB_URL = "https://github.com/denizsafak/abogen"
VERSION = get_version() VERSION = get_version()
# Settings
CHAPTER_OPTIONS_COUNTDOWN = 30 # Countdown seconds for chapter options
# Language description mapping # Language description mapping
LANGUAGE_DESCRIPTIONS = { LANGUAGE_DESCRIPTIONS = {
"a": "American English", "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.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
import soundfile as sf import soundfile as sf
from utils import clean_text, create_process from abogen.utils import clean_text, create_process
from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS, COLORS from abogen.constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS, COLORS, CHAPTER_OPTIONS_COUNTDOWN
from voice_formulas import get_new_voice from abogen.voice_formulas import get_new_voice
import hf_tracker import abogen.hf_tracker as hf_tracker
import static_ffmpeg import static_ffmpeg
import threading # for efficient waiting import threading # for efficient waiting
import subprocess import subprocess
def get_sample_voice_text(lang_code): def get_sample_voice_text(lang_code):
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"]) 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) layout.addWidget(self.merge_at_end_checkbox)
# Countdown label # 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 = QLabel(f"Auto-accepting in {self.countdown_seconds} seconds...")
self.countdown_label.setStyleSheet(f"color: {COLORS['GREEN']};") self.countdown_label.setStyleSheet(f"color: {COLORS['GREEN']};")
layout.addWidget(self.countdown_label) layout.addWidget(self.countdown_label)
+101 -20
View File
@@ -261,7 +261,7 @@ class InputBox(QLabel):
self.window().char_count = 0 self.window().char_count = 0
# embed icon at native size with word-wrap for the filename # embed icon at native size with word-wrap for the filename
self.setText( 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 # Set fixed width to force wrapping
self.setWordWrap(True) self.setWordWrap(True)
@@ -609,6 +609,8 @@ class abogen(QWidget):
self.displayed_file_path = ( self.displayed_file_path = (
None # Add new variable to track the 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.selected_chapters = set()
self.last_opened_book_path = None # Track the last opened book path self.last_opened_book_path = None # Track the last opened book path
self.last_output_path = None self.last_output_path = None
@@ -658,6 +660,9 @@ class abogen(QWidget):
self.queued_items = [] self.queued_items = []
self.current_queue_index = 0 self.current_queue_index = 0
# Log lines
self._log_lines = []
self.initUI() self.initUI()
self.speed_slider.setValue(int(self.config.get("speed", 1.00) * 100)) self.speed_slider.setValue(int(self.config.get("speed", 1.00) * 100))
self.update_speed_label() self.update_speed_label()
@@ -1367,6 +1372,7 @@ class abogen(QWidget):
self.input_box.hide() self.input_box.hide()
self.log_text.show() self.log_text.show()
self.log_text.clear() self.log_text.clear()
self._log_lines = []
QApplication.processEvents() QApplication.processEvents()
def restore_input_box(self): def restore_input_box(self):
@@ -1387,35 +1393,60 @@ class abogen(QWidget):
self._update_log_main_thread(message) self._update_log_main_thread(message)
def _update_log_main_thread(self, message): def _update_log_main_thread(self, message):
sb = self.log_text.verticalScrollBar() # Efficient log: keep only last 10 lines in a Python list
prev_val = sb.value() if not hasattr(self, '_log_lines'):
at_bottom = prev_val == sb.maximum() self._log_lines = []
# save current text cursor to preserve selection
old_cursor = self.log_text.textCursor() # Prepare the new log line as HTML
# prepare html
if isinstance(message, tuple): if isinstance(message, tuple):
text, spec = message text, spec = message
color = "green" if spec is True else ("red" if spec is False else spec) color = "green" if spec is True else ("red" if spec is False else spec)
html = self.color_text(text, color) html = self.color_text(text, color)
else: else:
html = str(message).replace("\n", "<br>") + "<br>" html = str(message).replace("\n", "<br>") + "<br>"
# move cursor to end for insertion
insert_cursor = self.log_text.textCursor() # Split incoming html into lines (preserving blank lines)
insert_cursor.movePosition(QTextCursor.End) new_lines = html.split("<br>")
self.log_text.setTextCursor(insert_cursor) # Remove the last empty string if html ends with <br>
self.log_text.insertHtml(html) if new_lines and new_lines[-1] == "":
# restore original cursor/selection new_lines = new_lines[:-1]
self.log_text.setTextCursor(old_cursor)
# restore scroll position # Append new lines to the buffer
sb.setValue(sb.maximum() if at_bottom else prev_val) 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() 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 def update_progress(self, value, etr_str): # Add etr_str parameter
# Ensure progress doesn't exceed 99% # Ensure progress doesn't exceed 99%
if value >= 100: if value >= 100:
value = 99 value = 99
self.progress_bar.setValue(value) 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( self.etr_label.setText(
f"Estimated time remaining: {etr_str}" f"Estimated time remaining: {etr_str}"
) # Update ETR label ) # Update ETR label
@@ -1442,7 +1473,7 @@ class abogen(QWidget):
self.btn_manage_queue.setStyleSheet("") self.btn_manage_queue.setStyleSheet("")
# Change main Start button to 'Start queue' if queue has items # Change main Start button to 'Start queue' if queue has items
if enabled: if enabled:
self.btn_start.setText("Start queue") self.btn_start.setText(f"Start queue ({len(self.queued_items)})")
try: try:
self.btn_start.clicked.disconnect() self.btn_start.clicked.disconnect()
except Exception: except Exception:
@@ -1537,6 +1568,11 @@ class abogen(QWidget):
def start_queue(self): def start_queue(self):
self.current_queue_index = 0 # Start from the first item 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() self.start_next_queued_item()
def start_next_queued_item(self): def start_next_queued_item(self):
@@ -1561,7 +1597,6 @@ class abogen(QWidget):
# Called after each conversion finishes # Called after each conversion finishes
self.current_queue_index += 1 self.current_queue_index += 1
if self.current_queue_index < len(self.queued_items): if self.current_queue_index < len(self.queued_items):
self.go_back_ui()
self.start_next_queued_item() self.start_next_queued_item()
else: else:
self.current_queue_index = 0 # Reset for next time self.current_queue_index = 0 # Reset for next time
@@ -1604,6 +1639,12 @@ class abogen(QWidget):
self.is_converting = True self.is_converting = True
self.convert_input_box_to_log() self.convert_input_box_to_log()
self.progress_bar.setValue(0) 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.progress_bar.setFormat("%p%") # Reset format initially
self.etr_label.hide() # Hide ETR label initially self.etr_label.hide() # Hide ETR label initially
self.controls_widget.hide() self.controls_widget.hide()
@@ -1767,11 +1808,20 @@ class abogen(QWidget):
if self.open_file_btn: if self.open_file_btn:
self.open_file_btn.setVisible(show_open_file_button) 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.controls_widget.hide()
self.finish_widget.show() self.finish_widget.show()
self.log_text.moveCursor(QTextCursor.End) self.log_text.moveCursor(QTextCursor.End)
self.log_text.ensureCursorVisible() self.log_text.ensureCursorVisible()
save_config(self.config) 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 # Start new queued item, if we're using a queued conversion
self.queue_item_conversion_finished() self.queue_item_conversion_finished()
@@ -1801,6 +1851,8 @@ class abogen(QWidget):
self.enable_disable_queue_buttons() self.enable_disable_queue_buttons()
self.restore_input_box() self.restore_input_box()
self.input_box.clear_input() # Reset text and style 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: except Exception as e:
self._show_error_message_box("Reset Error", f"Could not reset UI:\n{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.queue_row_widget.show() # Show queue row on go back
self.progress_bar.hide() self.progress_bar.hide()
self.restore_input_box() self.restore_input_box()
self.log_text.clear()
self._log_lines = []
# Use displayed_file_path instead of selected_file for EPUBs or PDFs # Use displayed_file_path instead of selected_file for EPUBs or PDFs
display_path = ( display_path = (
@@ -2187,6 +2241,8 @@ class abogen(QWidget):
self.queue_row_widget.show() # Show queue row on cancel self.queue_row_widget.show() # Show queue row on cancel
self.finish_widget.hide() self.finish_widget.hide()
self.restore_input_box() self.restore_input_box()
self.log_text.clear()
self._log_lines = []
display_path = ( display_path = (
self.displayed_file_path self.displayed_file_path
if 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) max_words_action.triggered.connect(self.set_max_subtitle_words)
menu.addAction(max_words_action) 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 # Add separator
menu.addSeparator() menu.addSeparator()
@@ -2947,6 +3005,29 @@ Categories=AudioVideo;Audio;Utility;
self, "Error", f"An error occurred while clearing temporary files:\n{e}" 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): def set_max_subtitle_words(self):
"""Open a dialog to set the maximum words per subtitle""" """Open a dialog to set the maximum words per subtitle"""
from PyQt5.QtWidgets import QInputDialog from PyQt5.QtWidgets import QInputDialog