diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6954003..35c28e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,9 @@
# v1.1.0 (pre-release)
+- Added queue system for processing multiple items, allowing users to add multiple files and process them in a queue, mentioned by @jborza in #30 (Special thanks to @jborza for implementing this feature in PR #35)
- 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.
+- Improvements in documentation and code.
# v1.0.9
- Added chunking/segmenting system that fixes memory outage issues when processing large audio files.
diff --git a/README.md b/README.md
index 9c8ca1d..41867df 100644
--- a/README.md
+++ b/README.md
@@ -119,34 +119,57 @@ abogen
Here’s Abogen in action: in this demo, it processes ∼3,000 characters of text in just 11 seconds and turns it into 3 minutes and 28 seconds of audio, and I have a low-end **RTX 2060 Mobile laptop GPU**. Your results may vary depending on your hardware.
-## `Key Features`
-- **Supported formats**: `ePub`, `PDF`, or `.TXT` files (or use built-in text editor)
-- **Speed**: Adjust speech rate from `0.1x` to `2.0x`
-- **Voices**: First letter of the language code (e.g., `a` for American English, `b` for British English, etc.), second letter is for `m` for male and `f` for female.
-- **Voice mixer**: Create custom voices by mixing different voice models with a profile system.
-- **Generate subtitles**: `Disabled`, `Sentence`, `Sentence + Comma`, `1 word`, `2 words`, `3 words`, etc. (Represents the number of words in each subtitle entry)
-- **Output formats**: `.WAV`, `.FLAC`, `.MP3`, `.OPUS` and `M4B (with chapters)` (Special thanks to [@jborza](https://github.com/jborza) for chapter support in PR [#10](https://github.com/denizsafak/abogen/pull/10))
-- **Save location**: `Save next to input file`, `Save to desktop`, or `Choose output folder`
-- **Chapter Control**: Select specific `chapters` from ePUBs or `chapters + pages` from PDFs.
-- **Save each chapter separately**: Save each chapter in e-books as a separate audio file.
-- **Project folder**: Save the converted items in a project folder with available metadata files.
-- **Options**:
- - **Replace single newlines with spaces**: Replaces single newlines with spaces in the text. This is useful for texts that have imaginary line breaks.
- - **Configure max words per subtitle**: Configures the maximum number of words per subtitle entry.
- - **Subtitle format**: Configures the subtitle format as `srt`, `ass (wide)`, `ass (narrow)`, `ass (centered wide)`, or `ass (centered narrow)`.
- - **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.
- - **Open temp directory**: Opens the temporary directory where converted text files are stored.
- - **Clear temporary files**: Deletes temporary files created during the conversion or preview.
- - **Check for updates at startup**: Automatically checks for updates when the program starts.
-- **After conversion**: `Open file`, `Go to folder`, `New conversion`, or `Go back`.
+## `Configuration`
+
+| Options | Description |
+|---------|-------------|
+| **Input Box** | Drag and drop `ePub`, `PDF`, or `.TXT` files (or use built-in text editor) |
+| **Queue options** | Add multiple files to a queue and process them in batch, with individual settings for each file. See [Queue mode](#queue-mode) for more details. |
+| **Speed** | Adjust speech rate from `0.1x` to `2.0x` |
+| **Select Voice** | First letter of the language code (e.g., `a` for American English, `b` for British English, etc.), second letter is for `m` for male and `f` for female. |
+| **Voice mixer** | Create custom voices by mixing different voice models with a profile system. See [Voice Mixer](#voice-mixer) for more details. |
+| **Voice preview** | Listen to the selected voice before processing. |
+| **Generate subtitles** | `Disabled`, `Sentence`, `Sentence + Comma`, `1 word`, `2 words`, `3 words`, etc. (Represents the number of words in each subtitle entry) |
+| **Output voice format** | `.WAV`, `.FLAC`, `.MP3`, `.OPUS (best compression)` and `M4B (with chapters)` (Special thanks to [@jborza](https://github.com/jborza) for chapter support in PR [#10](https://github.com/denizsafak/abogen/pull/10)) |
+| **Output subtitle format** | Configures the subtitle format as `SRT (standard)`, `ASS (wide)`, `ASS (narrow)`, `ASS (centered wide)`, or `ASS (centered narrow)`. |
+| **Replace single newlines with spaces** | Replaces single newlines with spaces in the text. This is useful for texts that have imaginary line breaks. |
+| **Save location** | `Save next to input file`, `Save to desktop`, or `Choose output folder` |
+
+| Book handler options | Description |
+|---------|-------------|
+| **Chapter Control** | Select specific `chapters` from ePUBs or `chapters + pages` from PDFs. |
+| **Save each chapter separately** | Save each chapter in e-books as a separate audio file. |
+| **Create a merged version** | Create a single audio file that combines all chapters. (If `Save each chapter separately` is disabled, this option will be the default behavior.) |
+| **Save in a project folder with metadata** | Save the converted items in a project folder with available metadata files. |
+
+| Menu options | Description |
+|---------|-------------|
+| **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. |
+| **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. |
+| **Open temp directory** | Opens the temporary directory where converted text files are stored. |
+| **Clear temporary files** | Deletes temporary files created during the conversion or preview. |
+| **Check for updates at startup** | Automatically checks for updates when the program starts. |
## `Voice Mixer`
With voice mixer, you can create custom voices by mixing different voice models. You can adjust the weight of each voice and save your custom voice as a profile for future use. The voice mixer allows you to create unique and personalized voices. (Huge thanks to [@jborza](https://github.com/jborza) for making this possible through his contributions in [#5](https://github.com/denizsafak/abogen/pull/5))
+## `Queue Mode`
+
+
+Abogen supports **queue mode**, allowing you to add multiple files to a processing queue. This is useful if you want to convert several files in one batch.
+
+- You can add text files (`.txt`) directly using the **Add files** button in the Queue Manager. To add PDF or EPUB files, use the input box in the main window and click the **Add to Queue** button.
+- Each file in the queue keeps the configuration settings that were active when it was added. Changing the main window configuration afterward does **not** affect files already in the queue.
+- You can view each file's configuration by hovering over them.
+
+Abogen will process each item in the queue automatically, saving outputs as configured.
+> Special thanks to [@jborza](https://github.com/jborza) for adding queue mode in PR [#35](https://github.com/denizsafak/abogen/pull/35)
+
## `About Chapter Markers`
When you process ePUB or PDF files, Abogen converts them into text files stored in your temporary directory. When you click "Edit," you're actually modifying these converted text files. In these text files, you'll notice tags that look like this:
@@ -257,12 +280,12 @@ Abogen is a standalone project, but it is inspired by and shares some similariti
- [ebook2audiobook](https://github.com/DrewThomasson/ebook2audiobook): Convert ebooks to audiobooks with chapters and metadata using dynamic AI models and voice cloning
## `Roadmap`
-- [ ] Add OCR scan feature for PDF files using docling.
+- [ ] Add OCR scan feature for PDF files using docling/teserract.
- [x] Add chapter metadata for .m4a files. (Issue [#9](https://github.com/denizsafak/abogen/issues/9), PR [#10](https://github.com/denizsafak/abogen/pull/10))
- [ ] Add support for different languages in GUI.
- [x] Add voice formula feature that enables mixing different voice models. (Issue [#1](https://github.com/denizsafak/abogen/issues/1), PR [#5](https://github.com/denizsafak/abogen/pull/5))
- [ ] Add support for kokoro-onnx (If it's necessary).
-- [ ] Add dark mode.
+- [x] Add dark mode.
## `Troubleshooting`
If you encounter any issues while running Abogen, try launching it from the command line with:
diff --git a/abogen/VERSION b/abogen/VERSION
index e5a4a5e..1cc5f65 100644
--- a/abogen/VERSION
+++ b/abogen/VERSION
@@ -1 +1 @@
-1.0.9
\ No newline at end of file
+1.1.0
\ No newline at end of file
diff --git a/abogen/constants.py b/abogen/constants.py
index 955bc4b..e20140f 100644
--- a/abogen/constants.py
+++ b/abogen/constants.py
@@ -1,4 +1,4 @@
-from utils import get_version
+from abogen.utils import get_version
# Program Information
PROGRAM_NAME = "abogen"
diff --git a/abogen/conversion.py b/abogen/conversion.py
index 0db8a83..1cbe1bc 100644
--- a/abogen/conversion.py
+++ b/abogen/conversion.py
@@ -6,11 +6,11 @@ import chardet
import charset_normalizer
import hashlib # For generating unique cache filenames
from platformdirs import user_desktop_dir
-from PyQt5.QtCore import QThread, pyqtSignal, Qt
+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
+from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS, COLORS
from voice_formulas import get_new_voice
import hf_tracker
import static_ffmpeg
@@ -61,7 +61,7 @@ class ChapterOptionsDialog(QDialog):
self.merge_at_end_checkbox = QCheckBox("Create a merged version at the end")
# Set default states
- self.save_separately_checkbox.setChecked(True)
+ self.save_separately_checkbox.setChecked(False)
self.merge_at_end_checkbox.setChecked(True)
# Connect checkbox state change signal
@@ -72,14 +72,36 @@ class ChapterOptionsDialog(QDialog):
layout.addWidget(self.save_separately_checkbox)
layout.addWidget(self.merge_at_end_checkbox)
+ # Countdown label
+ self.countdown_seconds = 30
+ 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)
+
# Add OK button
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
button_box.accepted.connect(self.accept)
layout.addWidget(button_box)
+ # Timer for countdown
+ self._timer = QTimer(self)
+ self._timer.timeout.connect(self._on_timer_tick)
+ self._timer.start(1000) # 1 second interval
+
+ # Store button_box for later use
+ self._button_box = button_box
+
# Initialize merge checkbox state
self.update_merge_checkbox_state()
+ def _on_timer_tick(self):
+ self.countdown_seconds -= 1
+ if self.countdown_seconds > 0:
+ self.countdown_label.setText(f"Auto-accepting in {self.countdown_seconds} seconds...")
+ else:
+ self._timer.stop()
+ self._button_box.accepted.emit() # Simulate OK click
+
def update_merge_checkbox_state(self):
# Enable merge checkbox only if save separately is checked
self.merge_at_end_checkbox.setEnabled(self.save_separately_checkbox.isChecked())
@@ -131,6 +153,7 @@ class ConversionThread(QThread):
start_time,
total_char_count,
use_gpu=True,
+ from_queue=False
): # Add use_gpu parameter
super().__init__()
self._chapter_options_event = threading.Event()
@@ -145,6 +168,7 @@ class ConversionThread(QThread):
self.subtitle_mode = subtitle_mode
self.cancel_requested = False
self.output_format = output_format
+ self.from_queue = from_queue
self.start_time = start_time # Store start_time
self.total_char_count = total_char_count # Use passed total character count
self.processed_char_count = 0 # Initialize processed character count
@@ -316,8 +340,13 @@ class ConversionThread(QThread):
hf_tracker.set_log_callback(lambda msg: self.log_updated.emit(msg))
# Show configuration
self.log_updated.emit("Configuration:")
- # Use display_path for logs if available, otherwise use the actual file name
- display_file = self.display_path if self.display_path else self.file_name
+
+ # Use file_name for logs if from_queue, otherwise use display_path if available
+ if getattr(self, "from_queue", False):
+ display_file = self.file_name
+ else:
+ display_file = self.display_path if self.display_path else self.file_name
+
self.log_updated.emit(f"- Input File: {display_file}")
# Use file size string passed from GUI
@@ -447,7 +476,14 @@ class ConversionThread(QThread):
save_chapters_separately = getattr(self, "save_chapters_separately", False)
chapters_out_dir = None
suffix = ""
- base_path = self.display_path if self.display_path else self.file_name
+
+
+ # Use file_name for logs if from_queue, otherwise use display_path if available
+ if getattr(self, "from_queue", False):
+ base_path = self.file_name
+ else:
+ base_path = self.display_path if self.display_path else self.file_name
+
base_name = os.path.splitext(os.path.basename(base_path))[0]
if self.save_option == "Save to Desktop":
parent_dir = user_desktop_dir()
@@ -946,8 +982,13 @@ class ConversionThread(QThread):
genre_match = re.search(r"<]*)>>", text)
# Use display path or filename as fallback for title
- filename = os.path.splitext(os.path.basename(self.display_path if self.display_path else self.file_name))[0]
+ # Use file_name for logs if from_queue, otherwise use display_path if available
+ if getattr(self, "from_queue", False):
+ filename = os.path.splitext(os.path.basename(self.file_name))[0]
+ else:
+ filename = os.path.splitext(os.path.basename(self.display_path if self.display_path else self.file_name))[0]
+
if title_match:
metadata_options.extend(["-metadata", f"title={title_match.group(1)}"])
else:
diff --git a/abogen/gui.py b/abogen/gui.py
index b670ba2..93209b6 100644
--- a/abogen/gui.py
+++ b/abogen/gui.py
@@ -6,7 +6,7 @@ import base64
import re
from abogen.queue_manager_gui import QueueManager
from abogen.queued_item import QueuedItem
-import hf_tracker
+import abogen.hf_tracker as hf_tracker
import hashlib # Added for cache path generation
from PyQt5.QtWidgets import (
QApplication,
@@ -53,7 +53,7 @@ from PyQt5.QtGui import (
QColor,
QMovie,
)
-from utils import (
+from abogen.utils import (
load_config,
save_config,
get_gpu_acceleration,
@@ -64,9 +64,9 @@ from utils import (
get_resource_path,
LoadPipelineThread,
)
-from conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
-from book_handler import HandlerDialog
-from constants import (
+from abogen.conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
+from abogen.book_handler import HandlerDialog
+from abogen.constants import (
PROGRAM_NAME,
VERSION,
GITHUB_URL,
@@ -77,8 +77,8 @@ from constants import (
COLORS
)
from threading import Thread
-from voice_formula_gui import VoiceFormulaDialog
-from voice_profiles import load_profiles
+from abogen.voice_formula_gui import VoiceFormulaDialog
+from abogen.voice_profiles import load_profiles
# Import ctypes for Windows-specific taskbar icon
if platform.system() == "Windows":
@@ -300,6 +300,9 @@ class InputBox(QLabel):
self.window().btn_add_to_queue.setEnabled(True)
self.chapters_btn.adjustSize()
+ # Reset the input_box_cleared_by_queue flag after setting file info
+ if hasattr(self.window(), 'input_box_cleared_by_queue'):
+ self.window().input_box_cleared_by_queue = False
def set_error(self, message):
self.setText(message)
@@ -329,6 +332,9 @@ class InputBox(QLabel):
# Disable add to queue button when input is cleared
if hasattr(self.window(), 'btn_add_to_queue'):
self.window().btn_add_to_queue.setEnabled(False)
+ # Reset the input_box_cleared_by_queue flag after setting file info
+ if hasattr(self.window(), 'input_box_cleared_by_queue'):
+ self.window().input_box_cleared_by_queue = True
def _human_readable_size(self, size, decimal_places=2):
for unit in ["B", "KB", "MB", "GB", "TB"]:
@@ -343,15 +349,38 @@ class InputBox(QLabel):
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
- ext = event.mimeData().urls()[0].toLocalFile().lower()
- if ext.endswith(".txt") or ext.endswith(".epub") or ext.endswith(".pdf"):
- event.acceptProposedAction()
- return
+ urls = event.mimeData().urls()
+ if urls:
+ ext = urls[0].toLocalFile().lower()
+ if ext.endswith(".txt") or ext.endswith(".epub") or ext.endswith(".pdf"):
+ event.acceptProposedAction()
+ # Set hover style based on current state
+ if self.styleSheet().find(self.STYLE_ACTIVE) != -1:
+ self.setStyleSheet(f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }} {self.STYLE_ACTIVE_HOVER}")
+ elif self.styleSheet().find(self.STYLE_ERROR) != -1:
+ self.setStyleSheet(f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }} {self.STYLE_ERROR_HOVER}")
+ else:
+ self.setStyleSheet(f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }} {self.STYLE_DEFAULT_HOVER}")
+ return
event.ignore()
+ def dragLeaveEvent(self, event):
+ # Restore the style based on current state
+ if self.styleSheet().find(self.STYLE_ACTIVE) != -1:
+ self.setStyleSheet(f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }}")
+ elif self.styleSheet().find(self.STYLE_ERROR) != -1:
+ self.setStyleSheet(f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }}")
+ else:
+ self.setStyleSheet(f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}")
+ event.accept()
+
def dropEvent(self, event):
if event.mimeData().hasUrls():
- file_path = event.mimeData().urls()[0].toLocalFile()
+ urls = event.mimeData().urls()
+ if not urls:
+ event.ignore()
+ return
+ file_path = urls[0].toLocalFile()
win = self.window()
if file_path.lower().endswith(".txt"):
win.selected_file, win.selected_file_type = file_path, "txt"
@@ -642,7 +671,7 @@ class abogen(QWidget):
self.voice_combo.setCurrentIndex(idx)
# If a profile is selected at startup, load voices and language
if self.selected_profile_name:
- from voice_profiles import load_profiles
+ from abogen.voice_profiles import load_profiles
entry = load_profiles().get(self.selected_profile_name, {})
if isinstance(entry, dict):
@@ -706,11 +735,11 @@ class abogen(QWidget):
self.btn_manage_queue.setEnabled(True)
self.btn_manage_queue.clicked.connect(self.manage_queue)
queue_row.addWidget(self.btn_manage_queue)
- self.btn_start_queue = QPushButton("Clear Queue", self)
- self.btn_start_queue.setFixedHeight(40)
- self.btn_start_queue.setEnabled(False)
- self.btn_start_queue.clicked.connect(self.clear_queue)
- queue_row.addWidget(self.btn_start_queue)
+ self.btn_clear_queue = QPushButton("Clear Queue", self)
+ self.btn_clear_queue.setFixedHeight(40)
+ self.btn_clear_queue.setEnabled(False)
+ self.btn_clear_queue.clicked.connect(self.clear_queue)
+ queue_row.addWidget(self.btn_clear_queue)
container_layout.addWidget(self.queue_row_widget)
self.log_text = QTextEdit(self)
self.log_text.setReadOnly(True)
@@ -1026,6 +1055,9 @@ class abogen(QWidget):
self.setLayout(outer_layout)
self.populate_profiles_in_voice_combo()
+ # Initialize flag to track if input box was cleared by queue
+ self.input_box_cleared_by_queue = False
+
def open_file_dialog(self):
if self.is_converting:
return
@@ -1255,7 +1287,7 @@ class abogen(QWidget):
if isinstance(data, str) and data.startswith("profile:"):
pname = data.split(":", 1)[1]
self.selected_profile_name = pname
- from voice_profiles import load_profiles
+ from abogen.voice_profiles import load_profiles
entry = load_profiles().get(pname, {})
# set mixed voices and language
@@ -1288,7 +1320,7 @@ class abogen(QWidget):
self.subtitle_combo.setEnabled(False)
def update_subtitle_combo_for_profile(self, profile_name):
- from voice_profiles import load_profiles
+ from abogen.voice_profiles import load_profiles
entry = load_profiles().get(profile_name, {})
lang = entry.get("language") if isinstance(entry, dict) else None
@@ -1398,7 +1430,7 @@ class abogen(QWidget):
def enable_disable_queue_buttons(self):
enabled = bool(self.queued_items)
- self.btn_start_queue.setEnabled(enabled)
+ self.btn_clear_queue.setEnabled(enabled)
# Update Manage Queue button text with count
if enabled:
self.btn_manage_queue.setText(f"Manage Queue ({len(self.queued_items)})")
@@ -1434,15 +1466,18 @@ class abogen(QWidget):
return self.queued_items
def add_to_queue(self):
- if not self.selected_file:
+ # Use the file currently displayed in the input box
+ file_to_queue = self.displayed_file_path if self.displayed_file_path else self.selected_file
+
+ if not file_to_queue:
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,
+ item_queue = QueuedItem(
+ file_name=file_to_queue,
lang_code=selected_lang,
speed=self.speed_slider.value() / 100.0,
voice=voice_formula,
@@ -1457,23 +1492,24 @@ class abogen(QWidget):
# Prevent adding duplicate items to the queue
for queued_item in self.queued_items:
if (
- queued_item.file_name == item.file_name and
- queued_item.lang_code == item.lang_code and
- queued_item.speed == item.speed and
- queued_item.voice == item.voice and
- queued_item.save_option == item.save_option and
- queued_item.output_folder == item.output_folder and
- queued_item.subtitle_mode == item.subtitle_mode and
- queued_item.output_format == item.output_format and
- queued_item.total_char_count == item.total_char_count and
- getattr(queued_item, "replace_single_newlines", False) == item.replace_single_newlines
+ queued_item.file_name == item_queue.file_name and
+ queued_item.lang_code == item_queue.lang_code and
+ queued_item.speed == item_queue.speed and
+ queued_item.voice == item_queue.voice and
+ queued_item.save_option == item_queue.save_option and
+ queued_item.output_folder == item_queue.output_folder and
+ queued_item.subtitle_mode == item_queue.subtitle_mode and
+ queued_item.output_format == item_queue.output_format and
+ getattr(queued_item, "replace_single_newlines", False) == item_queue.replace_single_newlines
):
QMessageBox.warning(self, "Duplicate Item", "This item is already in the queue.")
return
- self.enqueue(item)
+ print("Adding:", item_queue)
+ self.enqueue(item_queue)
# Clear input after adding to queue
self.input_box.clear_input()
+ self.input_box_cleared_by_queue = True # Set flag
self.enable_disable_queue_buttons()
def clear_queue(self):
@@ -1505,18 +1541,18 @@ class abogen(QWidget):
def start_next_queued_item(self):
if self.current_queue_index < len(self.queued_items):
- item = self.queued_items[self.current_queue_index]
- 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.replace_single_newlines = getattr(item, "replace_single_newlines", False)
- self.start_conversion()
+ queued_item = self.queued_items[self.current_queue_index]
+ self.selected_file = queued_item.file_name
+ self.selected_lang = queued_item.lang_code
+ self.speed_slider.setValue(int(queued_item.speed * 100))
+ self.selected_voice = queued_item.voice
+ self.save_option = queued_item.save_option
+ self.selected_output_folder = queued_item.output_folder
+ self.subtitle_mode = queued_item.subtitle_mode
+ self.selected_format = queued_item.output_format
+ self.char_count = queued_item.total_char_count
+ self.replace_single_newlines = getattr(queued_item, "replace_single_newlines", False)
+ self.start_conversion(from_queue=True)
else:
# Queue finished, reset index
self.current_queue_index = 0
@@ -1542,7 +1578,7 @@ class abogen(QWidget):
def get_selected_lang(self, voice_formula) -> str:
if self.selected_profile_name:
- from voice_profiles import load_profiles
+ from abogen.voice_profiles import load_profiles
entry = load_profiles().get(self.selected_profile_name, {})
selected_lang = entry.get("language")
else:
@@ -1560,7 +1596,7 @@ class abogen(QWidget):
else self.subtitle_mode
)
- def start_conversion(self):
+ def start_conversion(self, from_queue=False):
if not self.selected_file:
self.input_box.set_error("Please add a file.")
return
@@ -1624,6 +1660,7 @@ class abogen(QWidget):
start_time=self.start_time,
total_char_count=self.char_count,
use_gpu=self.gpu_ok,
+ from_queue=from_queue,
) # Use gpu_ok status
# Pass the displayed file path to the log_updated signal handler in ConversionThread
self.conversion_thread.display_path = display_path
@@ -1695,11 +1732,13 @@ class abogen(QWidget):
if self.displayed_file_path
else self.selected_file
)
- # Check if the file exists before trying to set file info
- if display_path and os.path.exists(display_path):
- self.input_box.set_file_info(display_path)
+ # Only repopulate if not cleared by queue
+ if not getattr(self, 'input_box_cleared_by_queue', False):
+ if display_path and os.path.exists(display_path):
+ self.input_box.set_file_info(display_path)
+ else:
+ self.input_box.clear_input()
else:
- # Clear the input if the file no longer exists
self.input_box.clear_input()
return
@@ -1738,8 +1777,6 @@ class abogen(QWidget):
def reset_ui(self):
try:
- self.restore_input_box()
- self.input_box.clear_input() # Reset text and style
self.etr_label.hide() # Hide ETR label
self.progress_bar.setValue(0)
self.progress_bar.hide()
@@ -1762,6 +1799,8 @@ class abogen(QWidget):
pass # Ignore error if not connected
self.btn_start.clicked.connect(self.start_conversion)
self.enable_disable_queue_buttons()
+ self.restore_input_box()
+ self.input_box.clear_input() # Reset text and style
except Exception as e:
self._show_error_message_box("Reset Error", f"Could not reset UI:\n{e}")
@@ -1777,11 +1816,13 @@ class abogen(QWidget):
self.displayed_file_path if self.displayed_file_path else self.selected_file
)
- # Check if the file exists before trying to set file info
- if display_path and os.path.exists(display_path):
- self.input_box.set_file_info(display_path)
+ # Only repopulate if not cleared by queue
+ if not getattr(self, 'input_box_cleared_by_queue', False):
+ if display_path and os.path.exists(display_path):
+ self.input_box.set_file_info(display_path)
+ else:
+ self.input_box.clear_input()
else:
- # Clear the input if the file no longer exists
self.input_box.clear_input()
# Ensure open file button is visible when going back
@@ -1868,7 +1909,7 @@ class abogen(QWidget):
voice_formula = " + ".join(filter(None, components))
voice_to_cache = voice_formula
if self.selected_profile_name:
- from voice_profiles import load_profiles
+ from abogen.voice_profiles import load_profiles
entry = load_profiles().get(self.selected_profile_name, {})
lang_to_cache = entry.get("language")
else:
@@ -2002,7 +2043,7 @@ class abogen(QWidget):
voice = " + ".join(filter(None, components))
# determine language: use profile setting, else explicit mixer selection, else fallback to first voice code
if self.selected_profile_name:
- from voice_profiles import load_profiles
+ from abogen.voice_profiles import load_profiles
entry = load_profiles().get(self.selected_profile_name, {})
lang = entry.get("language")
@@ -2151,11 +2192,13 @@ class abogen(QWidget):
if self.displayed_file_path
else self.selected_file
)
- # Check if the file exists before trying to set file info
- if display_path and os.path.exists(display_path):
- self.input_box.set_file_info(display_path)
+ # Only repopulate if not cleared by queue
+ if not getattr(self, 'input_box_cleared_by_queue', False):
+ if display_path and os.path.exists(display_path):
+ self.input_box.set_file_info(display_path)
+ else:
+ self.input_box.clear_input()
else:
- # Clear the input if the file no longer exists
self.input_box.clear_input()
if (
hasattr(self, "conversion_thread")
@@ -2213,7 +2256,7 @@ class abogen(QWidget):
def show_chapter_options_dialog(self, chapter_count):
"""Show dialog to ask user about chapter processing options when chapters are detected in a .txt file"""
- from conversion import ChapterOptionsDialog
+ from abogen.conversion import ChapterOptionsDialog
dialog = ChapterOptionsDialog(chapter_count, parent=self)
dialog.setWindowModality(Qt.ApplicationModal)
@@ -2458,7 +2501,7 @@ class abogen(QWidget):
def reveal_config_in_explorer(self):
"""Open the configuration file location in file explorer."""
- from utils import get_user_config_path
+ from abogen.utils import get_user_config_path
try:
config_path = get_user_config_path()
@@ -2490,7 +2533,7 @@ class abogen(QWidget):
"""Create a desktop shortcut to this program using PowerShell."""
import sys
from platformdirs import user_desktop_dir
- from utils import create_process
+ from abogen.utils import create_process
try:
if platform.system() == "Windows":
@@ -2625,7 +2668,7 @@ Categories=AudioVideo;Audio;Utility;
save_config(self.config)
def show_voice_formula_dialog(self):
- from voice_profiles import load_profiles
+ from abogen.voice_profiles import load_profiles
profiles = load_profiles()
initial_state = None
diff --git a/abogen/main.py b/abogen/main.py
index 2a311a9..d1d4a41 100644
--- a/abogen/main.py
+++ b/abogen/main.py
@@ -7,9 +7,9 @@ from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
# Add the directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
-from gui import abogen
-from utils import get_resource_path
-from constants import PROGRAM_NAME, VERSION
+from abogen.gui import abogen
+from abogen.utils import get_resource_path
+from abogen.constants import PROGRAM_NAME, VERSION
# Set environment variables for AMD ROCm
os.environ["MIOPEN_FIND_MODE"] = "FAST"
diff --git a/abogen/queue_manager_gui.py b/abogen/queue_manager_gui.py
index 5380a62..8e9ab1d 100644
--- a/abogen/queue_manager_gui.py
+++ b/abogen/queue_manager_gui.py
@@ -16,7 +16,7 @@ from PyQt5.QtWidgets import (
QSizePolicy,
)
from PyQt5.QtCore import QFileInfo, Qt
-from constants import COLORS
+from abogen.constants import COLORS
from copy import deepcopy
from PyQt5.QtGui import QFontMetrics
@@ -243,26 +243,23 @@ class QueueManager(QDialog):
items = self.listwidget.selectedItems()
if not items:
return
- import os
from PyQt5.QtWidgets import QMessageBox
- display_names = [item.text() for item in items]
- to_remove = []
- for q in self.queue:
- if os.path.basename(q.file_name) in display_names:
- to_remove.append(q)
+ # Remove by index to ensure correct mapping
+ rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
# Warn user if removing multiple files
- if len(to_remove) > 1:
+ if len(rows) > 1:
reply = QMessageBox.question(
self,
"Confirm Remove",
- f"Are you sure you want to remove {len(to_remove)} selected items from the queue?",
+ f"Are you sure you want to remove {len(rows)} selected items from the queue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply != QMessageBox.Yes:
return
- for item in to_remove:
- self.queue.remove(item)
+ for row in rows:
+ if 0 <= row < len(self.queue):
+ del self.queue[row]
self.process_queue()
self.update_button_states()
@@ -328,7 +325,7 @@ class QueueManager(QDialog):
return attrs
def add_files_from_paths(self, file_paths):
- from utils import calculate_text_length
+ from abogen.utils import calculate_text_length
from PyQt5.QtWidgets import QMessageBox
import os
current_attrs = self.get_current_attributes()
@@ -378,9 +375,8 @@ class QueueManager(QDialog):
self.update_button_states()
def add_more_files(self):
- from PyQt5.QtWidgets import QFileDialog, QMessageBox
- import os
- from utils import calculate_text_length # import the function
+ from PyQt5.QtWidgets import QFileDialog
+ from abogen.utils import calculate_text_length # import the function
# Only allow .txt files
files, _ = QFileDialog.getOpenFileNames(self, "Select .txt files", "", "Text Files (*.txt)")
if not files:
diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py
index a9d0d70..a6e09aa 100644
--- a/abogen/voice_formula_gui.py
+++ b/abogen/voice_formula_gui.py
@@ -28,7 +28,7 @@ from PyQt5.QtWidgets import (
)
from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize
from PyQt5.QtGui import QPixmap, QIcon
-from constants import (
+from abogen.constants import (
VOICES_INTERNAL,
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
LANGUAGE_DESCRIPTIONS,
@@ -36,8 +36,8 @@ from constants import (
)
import re
import platform
-from utils import get_resource_path
-from voice_profiles import (
+from abogen.utils import get_resource_path
+from abogen.voice_profiles import (
load_profiles,
save_profiles,
delete_profile,
@@ -1084,7 +1084,7 @@ class VoiceFormulaDialog(QDialog):
self, "Import Profiles", "", "JSON Files (*.json)"
)
if path:
- from voice_profiles import load_profiles, save_profiles
+ from abogen.voice_profiles import load_profiles, save_profiles
# Try to read the file and count profiles
try:
diff --git a/abogen/voice_formulas.py b/abogen/voice_formulas.py
index c28dfbe..af18c52 100644
--- a/abogen/voice_formulas.py
+++ b/abogen/voice_formulas.py
@@ -1,6 +1,5 @@
import re
-from constants import VOICES_INTERNAL
-
+from abogen.constants import VOICES_INTERNAL
# Calls parsing and loads the voice to gpu or cpu
def get_new_voice(pipeline, formula, use_gpu):
diff --git a/abogen/voice_profiles.py b/abogen/voice_profiles.py
index 637ecce..77a07d0 100644
--- a/abogen/voice_profiles.py
+++ b/abogen/voice_profiles.py
@@ -1,6 +1,6 @@
import os
import json
-from utils import get_user_config_path
+from abogen.utils import get_user_config_path
def _get_profiles_path():
diff --git a/demo/abogen.png b/demo/abogen.png
index b11c5dd..6ece4fc 100644
Binary files a/demo/abogen.png and b/demo/abogen.png differ
diff --git a/demo/abogen2.png b/demo/abogen2.png
index db31b34..6857f92 100644
Binary files a/demo/abogen2.png and b/demo/abogen2.png differ
diff --git a/demo/chapter_marker.png b/demo/chapter_marker.png
index 5e676fb..b5d62a6 100644
Binary files a/demo/chapter_marker.png and b/demo/chapter_marker.png differ
diff --git a/demo/queue.png b/demo/queue.png
new file mode 100644
index 0000000..bc16a2e
Binary files /dev/null and b/demo/queue.png differ
diff --git a/demo/voice_mixer.png b/demo/voice_mixer.png
index 6b40823..3949ae3 100644
Binary files a/demo/voice_mixer.png and b/demo/voice_mixer.png differ