mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 21:50:28 +02:00
Merge pull request #35 from jborza/main
Batch processing (multiple books in sequence)
This commit is contained in:
+160
-24
@@ -4,6 +4,8 @@ import tempfile
|
|||||||
import platform
|
import platform
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
|
from abogen.queue_manager_gui import QueueManager
|
||||||
|
from abogen.queued_item import QueuedItem
|
||||||
import hf_tracker
|
import hf_tracker
|
||||||
import hashlib # Added for cache path generation
|
import hashlib # Added for cache path generation
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
@@ -200,10 +202,22 @@ class InputBox(QLabel):
|
|||||||
name = os.path.basename(file_path)
|
name = os.path.basename(file_path)
|
||||||
char_count = "N/A"
|
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
|
# Format numbers with commas
|
||||||
def format_num(n):
|
def format_num(n):
|
||||||
try:
|
try:
|
||||||
return f"{int(n):,}"
|
if isinstance(n, str):
|
||||||
|
size = int(parse_size(n))
|
||||||
|
return f"{size:,}"
|
||||||
|
else:
|
||||||
|
return f"{n:,}"
|
||||||
except Exception:
|
except Exception:
|
||||||
return str(n)
|
return str(n)
|
||||||
|
|
||||||
@@ -592,6 +606,9 @@ class abogen(QWidget):
|
|||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("abogen")
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("abogen")
|
||||||
|
|
||||||
|
# Queued items list
|
||||||
|
self.queued_items = []
|
||||||
|
|
||||||
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()
|
||||||
@@ -840,6 +857,29 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
controls_layout.addLayout(gpu_layout)
|
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.setEnabled(False)
|
||||||
|
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.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)
|
||||||
|
|
||||||
# Start button
|
# Start button
|
||||||
self.btn_start = QPushButton("Start", self)
|
self.btn_start = QPushButton("Start", self)
|
||||||
self.btn_start.setFixedHeight(60)
|
self.btn_start.setFixedHeight(60)
|
||||||
@@ -926,6 +966,8 @@ class abogen(QWidget):
|
|||||||
file_path # Set the displayed file path for text files
|
file_path # Set the displayed file path for text files
|
||||||
)
|
)
|
||||||
self.input_box.set_file_info(file_path)
|
self.input_box.set_file_info(file_path)
|
||||||
|
# Enable add to queue button
|
||||||
|
self.btn_add_to_queue.setEnabled(True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._show_error_message_box(
|
self._show_error_message_box(
|
||||||
"File Dialog Error", f"Could not open file dialog:\n{e}"
|
"File Dialog Error", f"Could not open file dialog:\n{e}"
|
||||||
@@ -1271,6 +1313,118 @@ class abogen(QWidget):
|
|||||||
self.progress_bar.repaint()
|
self.progress_bar.repaint()
|
||||||
QApplication.processEvents()
|
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)
|
||||||
|
self.btn_add_to_queue.setEnabled(False)
|
||||||
|
|
||||||
|
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.")
|
||||||
|
return
|
||||||
|
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,
|
||||||
|
speed=self.speed_slider.value() / 100.0,
|
||||||
|
voice=voice_formula,
|
||||||
|
save_option=self.save_option,
|
||||||
|
output_folder=self.selected_output_folder,
|
||||||
|
subtitle_mode=actual_subtitle_mode,
|
||||||
|
output_format=self.selected_format,
|
||||||
|
total_char_count=self.char_count,
|
||||||
|
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:
|
||||||
|
self.input_box.set_error("Queue is empty.")
|
||||||
|
return
|
||||||
|
# show a dialog to manage the queue
|
||||||
|
dialog = QueueManager(self, self.queued_items)
|
||||||
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
|
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:
|
||||||
|
self.input_box.set_error("Queue is empty.")
|
||||||
|
return
|
||||||
|
# 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):
|
||||||
|
self.go_back_ui()
|
||||||
|
self.convert_first_queued_item()
|
||||||
|
|
||||||
|
def get_voice_formula(self) -> str:
|
||||||
|
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, voice_formula) -> str:
|
||||||
|
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
|
||||||
|
return selected_lang
|
||||||
|
|
||||||
|
def get_actual_subtitle_mode(self) -> str:
|
||||||
|
return (
|
||||||
|
"Disabled"
|
||||||
|
if not self.subtitle_combo.isEnabled()
|
||||||
|
else self.subtitle_mode
|
||||||
|
)
|
||||||
|
|
||||||
def start_conversion(self):
|
def start_conversion(self):
|
||||||
if not self.selected_file:
|
if not self.selected_file:
|
||||||
self.input_box.set_error("Please add a file.")
|
self.input_box.set_error("Please add a file.")
|
||||||
@@ -1313,32 +1467,12 @@ class abogen(QWidget):
|
|||||||
self.btn_cancel.setEnabled(True)
|
self.btn_cancel.setEnabled(True)
|
||||||
|
|
||||||
# Override subtitle_mode to "Disabled" if subtitle_combo is disabled
|
# Override subtitle_mode to "Disabled" if subtitle_combo is disabled
|
||||||
actual_subtitle_mode = (
|
actual_subtitle_mode = self.get_actual_subtitle_mode()
|
||||||
"Disabled"
|
|
||||||
if not self.subtitle_combo.isEnabled()
|
|
||||||
else self.subtitle_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
# if voice formula is not None, use the selected voice
|
# if voice formula is not None, use the selected voice
|
||||||
if self.mixed_voice_state:
|
voice_formula = self.get_voice_formula()
|
||||||
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
|
|
||||||
# determine selected language: use profile setting if profile selected, else voice code
|
# determine selected language: use profile setting if profile selected, else voice code
|
||||||
if self.selected_profile_name:
|
selected_lang = self.get_selected_lang(voice_formula)
|
||||||
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
|
|
||||||
|
|
||||||
self.conversion_thread = ConversionThread(
|
self.conversion_thread = ConversionThread(
|
||||||
self.selected_file,
|
self.selected_file,
|
||||||
@@ -1463,6 +1597,8 @@ class abogen(QWidget):
|
|||||||
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)
|
||||||
|
# Start new queued item, if we're using a queued conversion
|
||||||
|
self.queue_item_conversion_finished()
|
||||||
|
|
||||||
def reset_ui(self):
|
def reset_ui(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# 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:
|
||||||
|
# TODO take just the file name from the item, no directories
|
||||||
|
self.listwidget.addItem(item.file_name)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def get_queue(self):
|
||||||
|
return self.queue
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user