From d08ab54d2b59896158108d829b936acc33a8ccc7 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Mon, 30 Jun 2025 12:10:59 +0200 Subject: [PATCH 1/6] list of queued items, unfinished adding of queued items - #30 --- abogen/gui.py | 61 ++++++++++++++++++++++++++++++++++++++++++- abogen/queued_item.py | 15 +++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 abogen/queued_item.py diff --git a/abogen/gui.py b/abogen/gui.py index e7b8a85..5d4b37d 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -4,6 +4,7 @@ import tempfile import platform import base64 import re +from abogen.queued_item import QueuedItem import hf_tracker import hashlib # Added for cache path generation from PyQt5.QtWidgets import ( @@ -560,6 +561,9 @@ class abogen(QWidget): if platform.system() == "Windows": ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("abogen") + # Queued items list + self.queued_items = [] + self.initUI() self.speed_slider.setValue(int(self.config.get("speed", 1.00) * 100)) self.update_speed_label() @@ -808,6 +812,26 @@ class abogen(QWidget): controls_layout.addLayout(gpu_layout) + + # Manage queue button, start queue button + queue_row = QHBoxLayout() + # Add to queue button + self.btn_add_to_queue = QPushButton("Add to Queue", self) + self.btn_add_to_queue.setFixedHeight(40) + self.btn_add_to_queue.clicked.connect(self.add_to_queue) + queue_row.addWidget(self.btn_add_to_queue) + # Manage queue button + self.btn_manage_queue = QPushButton("Manage Queue", self) + self.btn_manage_queue.setFixedHeight(40) + self.btn_manage_queue.clicked.connect(self.manage_queue) + queue_row.addWidget(self.btn_manage_queue) + # Start queue button + self.btn_start_queue = QPushButton("Start Queue", self) + self.btn_start_queue.setFixedHeight(40) + self.btn_start_queue.clicked.connect(self.start_queue) + queue_row.addWidget(self.btn_start_queue) + controls_layout.addLayout(queue_row) + # Start button self.btn_start = QPushButton("Start", self) self.btn_start.setFixedHeight(60) @@ -982,7 +1006,7 @@ class abogen(QWidget): else: temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME) os.makedirs(temp_dir, exist_ok=True) - + fd, tmp = tempfile.mkstemp( prefix=f"{base_name}_", suffix=".txt", dir=temp_dir ) @@ -1239,6 +1263,41 @@ class abogen(QWidget): self.progress_bar.repaint() QApplication.processEvents() + def add_to_queue(self): + if not self.selected_file: + self.input_box.set_error("Please add a file.") + return + selected_lang = None + voice_formula = None + actual_subtitle_mode = None + + item = QueuedItem( + file_name=self.selected_file, + lang_code=selected_lang, # TODO copy from start_conversion + speed=self.speed_slider.value() / 100.0, + voice=voice_formula, # TODO copy from start_conversion + save_option=self.save_option, + output_folder=self.selected_output_folder, + subtitle_mode=actual_subtitle_mode, # TODO copy from start_conversion + output_format=self.selected_format, + total_char_count=self.char_count + ) + self.queued_items.append(item) + + def manage_queue(self): + if not self.queued_items: + self.input_box.set_error("Queue is empty.") + return + # TODO show a dialog to manage the queue + pass + + def start_queue(self): + if not self.queued_items: + self.input_box.set_error("Queue is empty.") + return + # TODO process the queue + pass + def start_conversion(self): if not self.selected_file: self.input_box.set_error("Please add a file.") diff --git a/abogen/queued_item.py b/abogen/queued_item.py new file mode 100644 index 0000000..635dd8f --- /dev/null +++ b/abogen/queued_item.py @@ -0,0 +1,15 @@ +# represents a queued item - book, chapters, voice, etc. +from dataclasses import dataclass + +@dataclass +class QueuedItem: + file_name: str + lang_code: str + speed: float + voice: str + save_option: str + output_folder: str + subtitle_mode: str + output_format: str + total_char_count: int + use_gpu: bool \ No newline at end of file From 17936a868e3aa936ad7bcf494b6feb8c5704f7e0 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Tue, 1 Jul 2025 13:31:47 +0200 Subject: [PATCH 2/6] queue manager UI - a simple listwidget - #30 --- abogen/gui.py | 31 ++++++++++++++--- abogen/queue_manager_gui.py | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 abogen/queue_manager_gui.py diff --git a/abogen/gui.py b/abogen/gui.py index 5d4b37d..6cdf0c1 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -4,6 +4,7 @@ import tempfile import platform import base64 import re +from abogen.queue_manager_gui import QueueManager from abogen.queued_item import QueuedItem import hf_tracker import hashlib # Added for cache path generation @@ -823,11 +824,13 @@ class abogen(QWidget): # Manage queue button self.btn_manage_queue = QPushButton("Manage Queue", self) self.btn_manage_queue.setFixedHeight(40) + self.btn_manage_queue.setEnabled(False) self.btn_manage_queue.clicked.connect(self.manage_queue) queue_row.addWidget(self.btn_manage_queue) # Start queue button self.btn_start_queue = QPushButton("Start Queue", self) self.btn_start_queue.setFixedHeight(40) + self.btn_start_queue.setEnabled(False) self.btn_start_queue.clicked.connect(self.start_queue) queue_row.addWidget(self.btn_start_queue) controls_layout.addLayout(queue_row) @@ -1263,6 +1266,20 @@ class abogen(QWidget): self.progress_bar.repaint() QApplication.processEvents() + def enable_disable_queue_buttons(self): + enabled = bool(self.queued_items) + self.btn_start_queue.setEnabled(enabled) + self.btn_manage_queue.setEnabled(enabled) + + def enqueue(self, item : QueuedItem): + self.queued_items.append(item) + self.update_log((f"Enqueued: {item.file_name}", True)) + # enable start queue button, manage queue button + self.enable_disable_queue_buttons() + + def get_queue(self): + return self.queued_items + def add_to_queue(self): if not self.selected_file: self.input_box.set_error("Please add a file.") @@ -1280,16 +1297,22 @@ class abogen(QWidget): output_folder=self.selected_output_folder, subtitle_mode=actual_subtitle_mode, # TODO copy from start_conversion output_format=self.selected_format, - total_char_count=self.char_count + total_char_count=self.char_count, + use_gpu=self.gpu_ok ) - self.queued_items.append(item) + self.enqueue(item) def manage_queue(self): if not self.queued_items: self.input_box.set_error("Queue is empty.") return - # TODO show a dialog to manage the queue - pass + # show a dialog to manage the queue + dialog = QueueManager(self, self.queued_items) + if dialog.exec_() == QDialog.Accepted: + # TODO take the new queue + self.queued_items = dialog.get_queue() + # re-enable/disable buttons based on queue state + self.enable_disable_queue_buttons() def start_queue(self): if not self.queued_items: diff --git a/abogen/queue_manager_gui.py b/abogen/queue_manager_gui.py new file mode 100644 index 0000000..313ee55 --- /dev/null +++ b/abogen/queue_manager_gui.py @@ -0,0 +1,69 @@ +# a simple window with a list of items in the queue, no checkboxes +# button to remove an item from the queue +# button to clear the queue + +from PyQt5.QtWidgets import ( + QWidget, + QDialog, + QVBoxLayout, + QDialogButtonBox, + QLabel, + QCheckBox, + QSlider, + QPushButton, + QHBoxLayout, + QMessageBox, + QListWidget, +) + + +class QueueManager(QDialog): + def __init__(self, parent, queue: list, title="Queue Manager", size=(400, 300)): + super().__init__() + self.queue = queue + self.parent = parent + layout = QVBoxLayout() + # list of queued items + self.listwidget = QListWidget() + self.listwidget.setSelectionMode(QListWidget.SingleSelection) + self.listwidget.setAlternatingRowColors(True) + # add queue items to the list + self.process_queue() + + layout.addWidget(self.listwidget) + # add remove item button + remove_button = QPushButton("Remove") + remove_button.clicked.connect(self.remove_item) + layout.addWidget(remove_button) + # add clear queue button + clear_button = QPushButton("Clear Queue") + clear_button.clicked.connect(self.clear_queue) + layout.addWidget(clear_button) + + + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + layout.addWidget(buttons) + + self.setLayout(layout) + + self.setWindowTitle(title) + self.setMinimumSize(*size) + + def process_queue(self): + """Process the queue items.""" + if not self.queue: + return + for item in self.queue: + self.listwidget.addItem(item.file_name) + + # self.listwidget.clicked.connect(self.clicked) + + def remove_item(self): + pass + + def clear_queue(self): + self.queue.clear() + self.listwidget.clear() \ No newline at end of file From ad91e0ec3442325766fa2959e7111d65d6407d47 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Wed, 2 Jul 2025 17:15:45 +0200 Subject: [PATCH 3/6] queue manager - can remove items now - #30 --- abogen/gui.py | 2 ++ abogen/queue_manager_gui.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/abogen/gui.py b/abogen/gui.py index 6cdf0c1..78527c0 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1301,6 +1301,8 @@ class abogen(QWidget): use_gpu=self.gpu_ok ) self.enqueue(item) + # Clear input after adding to queue + self.input_box.clear_input() def manage_queue(self): if not self.queued_items: diff --git a/abogen/queue_manager_gui.py b/abogen/queue_manager_gui.py index 313ee55..10ff38b 100644 --- a/abogen/queue_manager_gui.py +++ b/abogen/queue_manager_gui.py @@ -57,13 +57,23 @@ class QueueManager(QDialog): if not self.queue: return for item in self.queue: + # TODO take just the file name from the item, no directories self.listwidget.addItem(item.file_name) - # self.listwidget.clicked.connect(self.clicked) - def remove_item(self): + item = self.listwidget.currentItem() + if not item: + return + found_item = [q for q in self.queue if q.file_name==item.text()][0] + self.queue.remove(found_item) + # rebind the queue to the listwidget + self.listwidget.clear() + self.process_queue() pass def clear_queue(self): self.queue.clear() - self.listwidget.clear() \ No newline at end of file + self.listwidget.clear() + + def get_queue(self): + return self.queue \ No newline at end of file From bc474fecff27138b7308aceb9e812d2d2c8eee7f Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Wed, 2 Jul 2025 17:53:42 +0200 Subject: [PATCH 4/6] filled in lang, voice, subtitle mode for queued items, converting the queued items in sequence - #30 --- abogen/gui.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/abogen/gui.py b/abogen/gui.py index 78527c0..811e90e 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1284,9 +1284,9 @@ class abogen(QWidget): if not self.selected_file: self.input_box.set_error("Please add a file.") return - selected_lang = None - voice_formula = None - actual_subtitle_mode = None + selected_lang = self.get_selected_lang() + voice_formula = self.get_voice_formula() + actual_subtitle_mode = self.get_actual_subtitle_mode() item = QueuedItem( file_name=self.selected_file, @@ -1304,6 +1304,8 @@ class abogen(QWidget): # Clear input after adding to queue self.input_box.clear_input() + + def manage_queue(self): if not self.queued_items: self.input_box.set_error("Queue is empty.") @@ -1311,7 +1313,6 @@ class abogen(QWidget): # show a dialog to manage the queue dialog = QueueManager(self, self.queued_items) if dialog.exec_() == QDialog.Accepted: - # TODO take the new queue self.queued_items = dialog.get_queue() # re-enable/disable buttons based on queue state self.enable_disable_queue_buttons() @@ -1320,9 +1321,59 @@ class abogen(QWidget): if not self.queued_items: self.input_box.set_error("Queue is empty.") return - # TODO process the queue + # convert queued items, one by one - continued in queue_item_conversion_finished + self.convert_first_queued_item() pass + def convert_first_queued_item(self): + if not self.queued_items: + return + # Start conversion for the first item in the queue + item = self.queued_items[0] + self.selected_file = item.file_name + self.selected_lang = item.lang_code + self.speed_slider.setValue(int(item.speed * 100)) + self.selected_voice = item.voice + self.save_option = item.save_option + self.selected_output_folder = item.output_folder + self.subtitle_mode = item.subtitle_mode + self.selected_format = item.output_format + self.char_count = item.total_char_count + self.gpu_ok = item.use_gpu + # remove the first item from the queue + self.queued_items.pop(0) + + # Start conversion for the queued item + self.start_conversion() + + def queue_item_conversion_finished(self, message, output_path): + self.go_back_ui() + self.convert_first_queued_item() + + def get_voice_formula(self): + if self.mixed_voice_state: + formula_components = [ + f"{name}*{weight}" for name, weight in self.mixed_voice_state + ] + return " + ".join(filter(None, formula_components)) + else: + return self.selected_voice + + def get_selected_lang(self): + if self.selected_profile_name: + from voice_profiles import load_profiles + entry = load_profiles().get(self.selected_profile_name, {}) + return entry.get("language") + else: + return self.selected_voice[0] if self.selected_voice else None + + def get_actual_subtitle_mode(self): + return ( + "Disabled" + if not self.subtitle_combo.isEnabled() + else self.subtitle_mode + ) + def start_conversion(self): if not self.selected_file: self.input_box.set_error("Please add a file.") @@ -1439,6 +1490,9 @@ class abogen(QWidget): self.conversion_thread.conversion_finished.connect( self.on_conversion_finished ) + self.conversion_thread.conversion_finished.connect( + self.queue_item_conversion_finished + ) # Connect chapters_detected signal self.conversion_thread.chapters_detected.connect( From d0865015f975292ddb226fe595c4fcb730c5d3ce Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Tue, 8 Jul 2025 11:49:57 +0200 Subject: [PATCH 5/6] refactored setting selected_lang, actual_subtitle_mode, voice_formula in non-queued conversion - #30 --- abogen/gui.py | 56 ++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/abogen/gui.py b/abogen/gui.py index 811e90e..2e7478c 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1284,18 +1284,18 @@ class abogen(QWidget): if not self.selected_file: self.input_box.set_error("Please add a file.") return - selected_lang = self.get_selected_lang() - voice_formula = self.get_voice_formula() actual_subtitle_mode = self.get_actual_subtitle_mode() + voice_formula = self.get_voice_formula() + selected_lang = self.get_selected_lang(voice_formula) item = QueuedItem( file_name=self.selected_file, - lang_code=selected_lang, # TODO copy from start_conversion + lang_code=selected_lang, speed=self.speed_slider.value() / 100.0, - voice=voice_formula, # TODO copy from start_conversion + voice=voice_formula, save_option=self.save_option, output_folder=self.selected_output_folder, - subtitle_mode=actual_subtitle_mode, # TODO copy from start_conversion + subtitle_mode=actual_subtitle_mode, output_format=self.selected_format, total_char_count=self.char_count, use_gpu=self.gpu_ok @@ -1304,8 +1304,6 @@ class abogen(QWidget): # Clear input after adding to queue self.input_box.clear_input() - - def manage_queue(self): if not self.queued_items: self.input_box.set_error("Queue is empty.") @@ -1359,13 +1357,17 @@ class abogen(QWidget): else: return self.selected_voice - def get_selected_lang(self): + def get_selected_lang(self, voice_formula): if self.selected_profile_name: from voice_profiles import load_profiles entry = load_profiles().get(self.selected_profile_name, {}) - return entry.get("language") + selected_lang = entry.get("language") else: - return self.selected_voice[0] if self.selected_voice else None + selected_lang = self.selected_voice[0] if self.selected_voice else None + # fallback: extract from formula if missing + if not selected_lang: + m = re.search(r"\b([a-z])", voice_formula) + selected_lang = m.group(1) if m else None def get_actual_subtitle_mode(self): return ( @@ -1416,32 +1418,12 @@ class abogen(QWidget): self.btn_cancel.setEnabled(True) # Override subtitle_mode to "Disabled" if subtitle_combo is disabled - actual_subtitle_mode = ( - "Disabled" - if not self.subtitle_combo.isEnabled() - else self.subtitle_mode - ) + actual_subtitle_mode = self.get_actual_subtitle_mode() # if voice formula is not None, use the selected voice - if self.mixed_voice_state: - formula_components = [ - f"{name}*{weight}" for name, weight in self.mixed_voice_state - ] - voice_formula = " + ".join(filter(None, formula_components)) - else: - voice_formula = self.selected_voice + voice_formula = self.get_voice_formula() # determine selected language: use profile setting if profile selected, else voice code - if self.selected_profile_name: - from voice_profiles import load_profiles - - entry = load_profiles().get(self.selected_profile_name, {}) - selected_lang = entry.get("language") - else: - selected_lang = self.selected_voice[0] if self.selected_voice else None - # fallback: extract from formula if missing - if not selected_lang: - m = re.search(r"\b([a-z])", voice_formula) - selected_lang = m.group(1) if m else None + selected_lang = self.get_selected_lang(voice_formula) self.conversion_thread = ConversionThread( self.selected_file, @@ -1490,9 +1472,10 @@ class abogen(QWidget): self.conversion_thread.conversion_finished.connect( self.on_conversion_finished ) - self.conversion_thread.conversion_finished.connect( - self.queue_item_conversion_finished - ) + # TODO remove + # self.conversion_thread.conversion_finished.connect( + # self.queue_item_conversion_finished + # ) # Connect chapters_detected signal self.conversion_thread.chapters_detected.connect( @@ -1569,6 +1552,7 @@ class abogen(QWidget): self.log_text.moveCursor(QTextCursor.End) self.log_text.ensureCursorVisible() save_config(self.config) + self.queue_item_conversion_finished() def reset_ui(self): try: From 95f647fb546f4ecabb2f387b1486cd9168a29ac8 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Tue, 8 Jul 2025 13:02:16 +0200 Subject: [PATCH 6/6] disabling add to queue button on start / after queuing an item - fixes #30 --- abogen/gui.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/abogen/gui.py b/abogen/gui.py index 2e7478c..f83910a 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -185,10 +185,22 @@ class InputBox(QLabel): name = os.path.basename(file_path) char_count = "N/A" + def parse_size(size_str): + # Use regex to extract the numeric part + match = re.match(r"([\d.]+)", size_str) + if match: + return float(match.group(1)) + raise ValueError(f"Invalid size format: {size_str}") + + # Format numbers with commas def format_num(n): try: - return f"{int(n):,}" + if isinstance(n, str): + size = int(parse_size(n)) + return f"{size:,}" + else: + return f"{n:,}" except Exception: return str(n) @@ -819,6 +831,7 @@ class abogen(QWidget): # Add to queue button self.btn_add_to_queue = QPushButton("Add to Queue", self) self.btn_add_to_queue.setFixedHeight(40) + self.btn_add_to_queue.setEnabled(False) self.btn_add_to_queue.clicked.connect(self.add_to_queue) queue_row.addWidget(self.btn_add_to_queue) # Manage queue button @@ -921,6 +934,8 @@ class abogen(QWidget): file_path # Set the displayed file path for text files ) self.input_box.set_file_info(file_path) + # Enable add to queue button + self.btn_add_to_queue.setEnabled(True) except Exception as e: self._show_error_message_box( "File Dialog Error", f"Could not open file dialog:\n{e}" @@ -1270,6 +1285,7 @@ class abogen(QWidget): enabled = bool(self.queued_items) self.btn_start_queue.setEnabled(enabled) self.btn_manage_queue.setEnabled(enabled) + self.btn_add_to_queue.setEnabled(False) def enqueue(self, item : QueuedItem): self.queued_items.append(item) @@ -1344,11 +1360,11 @@ class abogen(QWidget): # Start conversion for the queued item self.start_conversion() - def queue_item_conversion_finished(self, message, output_path): + def queue_item_conversion_finished(self): self.go_back_ui() self.convert_first_queued_item() - def get_voice_formula(self): + def get_voice_formula(self) -> str: if self.mixed_voice_state: formula_components = [ f"{name}*{weight}" for name, weight in self.mixed_voice_state @@ -1357,7 +1373,7 @@ class abogen(QWidget): else: return self.selected_voice - def get_selected_lang(self, voice_formula): + def get_selected_lang(self, voice_formula) -> str: if self.selected_profile_name: from voice_profiles import load_profiles entry = load_profiles().get(self.selected_profile_name, {}) @@ -1368,8 +1384,9 @@ class abogen(QWidget): if not selected_lang: m = re.search(r"\b([a-z])", voice_formula) selected_lang = m.group(1) if m else None + return selected_lang - def get_actual_subtitle_mode(self): + def get_actual_subtitle_mode(self) -> str: return ( "Disabled" if not self.subtitle_combo.isEnabled() @@ -1472,10 +1489,6 @@ class abogen(QWidget): self.conversion_thread.conversion_finished.connect( self.on_conversion_finished ) - # TODO remove - # self.conversion_thread.conversion_finished.connect( - # self.queue_item_conversion_finished - # ) # Connect chapters_detected signal self.conversion_thread.chapters_detected.connect( @@ -1552,6 +1565,7 @@ class abogen(QWidget): self.log_text.moveCursor(QTextCursor.End) self.log_text.ensureCursorVisible() save_config(self.config) + # Start new queued item, if we're using a queued conversion self.queue_item_conversion_finished() def reset_ui(self):