mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
Added chapter options countdown and new option "Configure max lines in log window", improvements in code
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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. |
|
| **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. |
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+107
-26
@@ -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,7 +1639,13 @@ 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)
|
||||||
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.etr_label.hide() # Hide ETR label initially
|
||||||
self.controls_widget.hide()
|
self.controls_widget.hide()
|
||||||
self.queue_row_widget.hide() # Hide queue row when process starts
|
self.queue_row_widget.hide() # Hide queue row when process starts
|
||||||
@@ -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)
|
||||||
|
|
||||||
self.controls_widget.hide()
|
# Only show finish_widget if queue is done ---
|
||||||
self.finish_widget.show()
|
if self.current_queue_index + 1 >= len(self.queued_items) or not self.queued_items:
|
||||||
self.log_text.moveCursor(QTextCursor.End)
|
# Queue finished, show finish screen
|
||||||
self.log_text.ensureCursorVisible()
|
self.controls_widget.hide()
|
||||||
save_config(self.config)
|
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
|
# 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
|
||||||
|
|||||||
Reference in New Issue
Block a user