Various improvements and modifications in code and documentation

This commit is contained in:
Deniz Şafak
2025-07-10 23:27:19 +03:00
parent 0c7a52fd86
commit 6ede337658
16 changed files with 232 additions and 127 deletions
+3
View File
@@ -1,6 +1,9 @@
# v1.1.0 (pre-release) # 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 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.
- Improvements in documentation and code.
# v1.0.9 # v1.0.9
- Added chunking/segmenting system that fixes memory outage issues when processing large audio files. - Added chunking/segmenting system that fixes memory outage issues when processing large audio files.
+47 -24
View File
@@ -119,34 +119,57 @@ abogen
Heres 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. Heres 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` ## `Configuration`
- **Supported formats**: `ePub`, `PDF`, or `.TXT` files (or use built-in text editor)
- **Speed**: Adjust speech rate from `0.1x` to `2.0x` | Options | Description |
- **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. | **Input Box** | Drag and drop `ePub`, `PDF`, or `.TXT` files (or use built-in text editor) |
- **Generate subtitles**: `Disabled`, `Sentence`, `Sentence + Comma`, `1 word`, `2 words`, `3 words`, etc. (Represents the number of words in each subtitle entry) | **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. |
- **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)) | **Speed** | Adjust speech rate from `0.1x` to `2.0x` |
- **Save location**: `Save next to input file`, `Save to desktop`, or `Choose output folder` | **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. |
- **Chapter Control**: Select specific `chapters` from ePUBs or `chapters + pages` from PDFs. | **Voice mixer** | Create custom voices by mixing different voice models with a profile system. See [Voice Mixer](#voice-mixer) for more details. |
- **Save each chapter separately**: Save each chapter in e-books as a separate audio file. | **Voice preview** | Listen to the selected voice before processing. |
- **Project folder**: Save the converted items in a project folder with available metadata files. | **Generate subtitles** | `Disabled`, `Sentence`, `Sentence + Comma`, `1 word`, `2 words`, `3 words`, etc. (Represents the number of words in each subtitle entry) |
- **Options**: | **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)) |
- **Replace single newlines with spaces**: Replaces single newlines with spaces in the text. This is useful for texts that have imaginary line breaks. | **Output subtitle format** | Configures the subtitle format as `SRT (standard)`, `ASS (wide)`, `ASS (narrow)`, `ASS (centered wide)`, or `ASS (centered narrow)`. |
- **Configure max words per subtitle**: Configures the maximum number of words per subtitle entry. | **Replace single newlines with spaces** | Replaces single newlines with spaces in the text. This is useful for texts that have imaginary line breaks. |
- **Subtitle format**: Configures the subtitle format as `srt`, `ass (wide)`, `ass (narrow)`, `ass (centered wide)`, or `ass (centered narrow)`. | **Save location** | `Save next to input file`, `Save to desktop`, or `Choose output folder` |
- **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. | Book handler options | Description |
- **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. | **Chapter Control** | Select specific `chapters` from ePUBs or `chapters + pages` from PDFs. |
- **Clear temporary files**: Deletes temporary files created during the conversion or preview. | **Save each chapter separately** | Save each chapter in e-books as a separate audio file. |
- **Check for updates at startup**: Automatically checks for updates when the program starts. | **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.) |
- **After conversion**: `Open file`, `Go to folder`, `New conversion`, or `Go back`. | **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` ## `Voice Mixer`
<img title="Abogen Voice Mixer" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/voice_mixer.png'> <img title="Abogen Voice Mixer" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/voice_mixer.png'>
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)) 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`
<img title="Abogen queue mode" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/queue.png'>
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` ## `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: 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 - [ebook2audiobook](https://github.com/DrewThomasson/ebook2audiobook): Convert ebooks to audiobooks with chapters and metadata using dynamic AI models and voice cloning
## `Roadmap` ## `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)) - [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. - [ ] 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)) - [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 support for kokoro-onnx (If it's necessary).
- [ ] Add dark mode. - [x] Add dark mode.
## `Troubleshooting` ## `Troubleshooting`
If you encounter any issues while running Abogen, try launching it from the command line with: If you encounter any issues while running Abogen, try launching it from the command line with:
+1 -1
View File
@@ -1 +1 @@
1.0.9 1.1.0
+1 -1
View File
@@ -1,4 +1,4 @@
from utils import get_version from abogen.utils import get_version
# Program Information # Program Information
PROGRAM_NAME = "abogen" PROGRAM_NAME = "abogen"
+48 -7
View File
@@ -6,11 +6,11 @@ import chardet
import charset_normalizer import charset_normalizer
import hashlib # For generating unique cache filenames import hashlib # For generating unique cache filenames
from platformdirs import user_desktop_dir 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 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 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 from voice_formulas import get_new_voice
import hf_tracker import hf_tracker
import static_ffmpeg import static_ffmpeg
@@ -61,7 +61,7 @@ class ChapterOptionsDialog(QDialog):
self.merge_at_end_checkbox = QCheckBox("Create a merged version at the end") self.merge_at_end_checkbox = QCheckBox("Create a merged version at the end")
# Set default states # Set default states
self.save_separately_checkbox.setChecked(True) self.save_separately_checkbox.setChecked(False)
self.merge_at_end_checkbox.setChecked(True) self.merge_at_end_checkbox.setChecked(True)
# Connect checkbox state change signal # Connect checkbox state change signal
@@ -72,14 +72,36 @@ class ChapterOptionsDialog(QDialog):
layout.addWidget(self.save_separately_checkbox) layout.addWidget(self.save_separately_checkbox)
layout.addWidget(self.merge_at_end_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 # Add OK button
button_box = QDialogButtonBox(QDialogButtonBox.Ok) button_box = QDialogButtonBox(QDialogButtonBox.Ok)
button_box.accepted.connect(self.accept) button_box.accepted.connect(self.accept)
layout.addWidget(button_box) 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 # Initialize merge checkbox state
self.update_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): def update_merge_checkbox_state(self):
# Enable merge checkbox only if save separately is checked # Enable merge checkbox only if save separately is checked
self.merge_at_end_checkbox.setEnabled(self.save_separately_checkbox.isChecked()) self.merge_at_end_checkbox.setEnabled(self.save_separately_checkbox.isChecked())
@@ -131,6 +153,7 @@ class ConversionThread(QThread):
start_time, start_time,
total_char_count, total_char_count,
use_gpu=True, use_gpu=True,
from_queue=False
): # Add use_gpu parameter ): # Add use_gpu parameter
super().__init__() super().__init__()
self._chapter_options_event = threading.Event() self._chapter_options_event = threading.Event()
@@ -145,6 +168,7 @@ class ConversionThread(QThread):
self.subtitle_mode = subtitle_mode self.subtitle_mode = subtitle_mode
self.cancel_requested = False self.cancel_requested = False
self.output_format = output_format self.output_format = output_format
self.from_queue = from_queue
self.start_time = start_time # Store start_time self.start_time = start_time # Store start_time
self.total_char_count = total_char_count # Use passed total character count self.total_char_count = total_char_count # Use passed total character count
self.processed_char_count = 0 # Initialize processed 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)) hf_tracker.set_log_callback(lambda msg: self.log_updated.emit(msg))
# Show configuration # Show configuration
self.log_updated.emit("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}") self.log_updated.emit(f"- Input File: {display_file}")
# Use file size string passed from GUI # Use file size string passed from GUI
@@ -447,7 +476,14 @@ class ConversionThread(QThread):
save_chapters_separately = getattr(self, "save_chapters_separately", False) save_chapters_separately = getattr(self, "save_chapters_separately", False)
chapters_out_dir = None chapters_out_dir = None
suffix = "" 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] base_name = os.path.splitext(os.path.basename(base_path))[0]
if self.save_option == "Save to Desktop": if self.save_option == "Save to Desktop":
parent_dir = user_desktop_dir() parent_dir = user_desktop_dir()
@@ -946,8 +982,13 @@ class ConversionThread(QThread):
genre_match = re.search(r"<<METADATA_GENRE:([^>]*)>>", text) genre_match = re.search(r"<<METADATA_GENRE:([^>]*)>>", text)
# Use display path or filename as fallback for title # 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: if title_match:
metadata_options.extend(["-metadata", f"title={title_match.group(1)}"]) metadata_options.extend(["-metadata", f"title={title_match.group(1)}"])
else: else:
+112 -69
View File
@@ -6,7 +6,7 @@ import base64
import re import re
from abogen.queue_manager_gui import QueueManager from abogen.queue_manager_gui import QueueManager
from abogen.queued_item import QueuedItem from abogen.queued_item import QueuedItem
import hf_tracker import abogen.hf_tracker as hf_tracker
import hashlib # Added for cache path generation import hashlib # Added for cache path generation
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QApplication,
@@ -53,7 +53,7 @@ from PyQt5.QtGui import (
QColor, QColor,
QMovie, QMovie,
) )
from utils import ( from abogen.utils import (
load_config, load_config,
save_config, save_config,
get_gpu_acceleration, get_gpu_acceleration,
@@ -64,9 +64,9 @@ from utils import (
get_resource_path, get_resource_path,
LoadPipelineThread, LoadPipelineThread,
) )
from conversion import ConversionThread, VoicePreviewThread, PlayAudioThread from abogen.conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
from book_handler import HandlerDialog from abogen.book_handler import HandlerDialog
from constants import ( from abogen.constants import (
PROGRAM_NAME, PROGRAM_NAME,
VERSION, VERSION,
GITHUB_URL, GITHUB_URL,
@@ -77,8 +77,8 @@ from constants import (
COLORS COLORS
) )
from threading import Thread from threading import Thread
from voice_formula_gui import VoiceFormulaDialog from abogen.voice_formula_gui import VoiceFormulaDialog
from voice_profiles import load_profiles from abogen.voice_profiles import load_profiles
# Import ctypes for Windows-specific taskbar icon # Import ctypes for Windows-specific taskbar icon
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -300,6 +300,9 @@ class InputBox(QLabel):
self.window().btn_add_to_queue.setEnabled(True) self.window().btn_add_to_queue.setEnabled(True)
self.chapters_btn.adjustSize() 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): def set_error(self, message):
self.setText(message) self.setText(message)
@@ -329,6 +332,9 @@ class InputBox(QLabel):
# Disable add to queue button when input is cleared # Disable add to queue button when input is cleared
if hasattr(self.window(), 'btn_add_to_queue'): if hasattr(self.window(), 'btn_add_to_queue'):
self.window().btn_add_to_queue.setEnabled(False) 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): def _human_readable_size(self, size, decimal_places=2):
for unit in ["B", "KB", "MB", "GB", "TB"]: for unit in ["B", "KB", "MB", "GB", "TB"]:
@@ -343,15 +349,38 @@ class InputBox(QLabel):
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
if event.mimeData().hasUrls(): if event.mimeData().hasUrls():
ext = event.mimeData().urls()[0].toLocalFile().lower() urls = event.mimeData().urls()
if ext.endswith(".txt") or ext.endswith(".epub") or ext.endswith(".pdf"): if urls:
event.acceptProposedAction() ext = urls[0].toLocalFile().lower()
return 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() 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): def dropEvent(self, event):
if event.mimeData().hasUrls(): 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() win = self.window()
if file_path.lower().endswith(".txt"): if file_path.lower().endswith(".txt"):
win.selected_file, win.selected_file_type = file_path, "txt" win.selected_file, win.selected_file_type = file_path, "txt"
@@ -642,7 +671,7 @@ class abogen(QWidget):
self.voice_combo.setCurrentIndex(idx) self.voice_combo.setCurrentIndex(idx)
# If a profile is selected at startup, load voices and language # If a profile is selected at startup, load voices and language
if self.selected_profile_name: 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, {}) entry = load_profiles().get(self.selected_profile_name, {})
if isinstance(entry, dict): if isinstance(entry, dict):
@@ -706,11 +735,11 @@ class abogen(QWidget):
self.btn_manage_queue.setEnabled(True) self.btn_manage_queue.setEnabled(True)
self.btn_manage_queue.clicked.connect(self.manage_queue) self.btn_manage_queue.clicked.connect(self.manage_queue)
queue_row.addWidget(self.btn_manage_queue) queue_row.addWidget(self.btn_manage_queue)
self.btn_start_queue = QPushButton("Clear Queue", self) self.btn_clear_queue = QPushButton("Clear Queue", self)
self.btn_start_queue.setFixedHeight(40) self.btn_clear_queue.setFixedHeight(40)
self.btn_start_queue.setEnabled(False) self.btn_clear_queue.setEnabled(False)
self.btn_start_queue.clicked.connect(self.clear_queue) self.btn_clear_queue.clicked.connect(self.clear_queue)
queue_row.addWidget(self.btn_start_queue) queue_row.addWidget(self.btn_clear_queue)
container_layout.addWidget(self.queue_row_widget) container_layout.addWidget(self.queue_row_widget)
self.log_text = QTextEdit(self) self.log_text = QTextEdit(self)
self.log_text.setReadOnly(True) self.log_text.setReadOnly(True)
@@ -1026,6 +1055,9 @@ class abogen(QWidget):
self.setLayout(outer_layout) self.setLayout(outer_layout)
self.populate_profiles_in_voice_combo() 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): def open_file_dialog(self):
if self.is_converting: if self.is_converting:
return return
@@ -1255,7 +1287,7 @@ class abogen(QWidget):
if isinstance(data, str) and data.startswith("profile:"): if isinstance(data, str) and data.startswith("profile:"):
pname = data.split(":", 1)[1] pname = data.split(":", 1)[1]
self.selected_profile_name = pname self.selected_profile_name = pname
from voice_profiles import load_profiles from abogen.voice_profiles import load_profiles
entry = load_profiles().get(pname, {}) entry = load_profiles().get(pname, {})
# set mixed voices and language # set mixed voices and language
@@ -1288,7 +1320,7 @@ class abogen(QWidget):
self.subtitle_combo.setEnabled(False) self.subtitle_combo.setEnabled(False)
def update_subtitle_combo_for_profile(self, profile_name): 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, {}) entry = load_profiles().get(profile_name, {})
lang = entry.get("language") if isinstance(entry, dict) else None lang = entry.get("language") if isinstance(entry, dict) else None
@@ -1398,7 +1430,7 @@ class abogen(QWidget):
def enable_disable_queue_buttons(self): def enable_disable_queue_buttons(self):
enabled = bool(self.queued_items) enabled = bool(self.queued_items)
self.btn_start_queue.setEnabled(enabled) self.btn_clear_queue.setEnabled(enabled)
# Update Manage Queue button text with count # Update Manage Queue button text with count
if enabled: if enabled:
self.btn_manage_queue.setText(f"Manage Queue ({len(self.queued_items)})") self.btn_manage_queue.setText(f"Manage Queue ({len(self.queued_items)})")
@@ -1434,15 +1466,18 @@ class abogen(QWidget):
return self.queued_items return self.queued_items
def add_to_queue(self): 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.") self.input_box.set_error("Please add a file.")
return return
actual_subtitle_mode = self.get_actual_subtitle_mode() actual_subtitle_mode = self.get_actual_subtitle_mode()
voice_formula = self.get_voice_formula() voice_formula = self.get_voice_formula()
selected_lang = self.get_selected_lang(voice_formula) selected_lang = self.get_selected_lang(voice_formula)
item = QueuedItem( item_queue = QueuedItem(
file_name=self.selected_file, file_name=file_to_queue,
lang_code=selected_lang, lang_code=selected_lang,
speed=self.speed_slider.value() / 100.0, speed=self.speed_slider.value() / 100.0,
voice=voice_formula, voice=voice_formula,
@@ -1457,23 +1492,24 @@ class abogen(QWidget):
# Prevent adding duplicate items to the queue # Prevent adding duplicate items to the queue
for queued_item in self.queued_items: for queued_item in self.queued_items:
if ( if (
queued_item.file_name == item.file_name and queued_item.file_name == item_queue.file_name and
queued_item.lang_code == item.lang_code and queued_item.lang_code == item_queue.lang_code and
queued_item.speed == item.speed and queued_item.speed == item_queue.speed and
queued_item.voice == item.voice and queued_item.voice == item_queue.voice and
queued_item.save_option == item.save_option and queued_item.save_option == item_queue.save_option and
queued_item.output_folder == item.output_folder and queued_item.output_folder == item_queue.output_folder and
queued_item.subtitle_mode == item.subtitle_mode and queued_item.subtitle_mode == item_queue.subtitle_mode and
queued_item.output_format == item.output_format and queued_item.output_format == item_queue.output_format and
queued_item.total_char_count == item.total_char_count and getattr(queued_item, "replace_single_newlines", False) == item_queue.replace_single_newlines
getattr(queued_item, "replace_single_newlines", False) == item.replace_single_newlines
): ):
QMessageBox.warning(self, "Duplicate Item", "This item is already in the queue.") QMessageBox.warning(self, "Duplicate Item", "This item is already in the queue.")
return return
self.enqueue(item) print("Adding:", item_queue)
self.enqueue(item_queue)
# Clear input after adding to queue # Clear input after adding to queue
self.input_box.clear_input() self.input_box.clear_input()
self.input_box_cleared_by_queue = True # Set flag
self.enable_disable_queue_buttons() self.enable_disable_queue_buttons()
def clear_queue(self): def clear_queue(self):
@@ -1505,18 +1541,18 @@ class abogen(QWidget):
def start_next_queued_item(self): def start_next_queued_item(self):
if self.current_queue_index < len(self.queued_items): if self.current_queue_index < len(self.queued_items):
item = self.queued_items[self.current_queue_index] queued_item = self.queued_items[self.current_queue_index]
self.selected_file = item.file_name self.selected_file = queued_item.file_name
self.selected_lang = item.lang_code self.selected_lang = queued_item.lang_code
self.speed_slider.setValue(int(item.speed * 100)) self.speed_slider.setValue(int(queued_item.speed * 100))
self.selected_voice = item.voice self.selected_voice = queued_item.voice
self.save_option = item.save_option self.save_option = queued_item.save_option
self.selected_output_folder = item.output_folder self.selected_output_folder = queued_item.output_folder
self.subtitle_mode = item.subtitle_mode self.subtitle_mode = queued_item.subtitle_mode
self.selected_format = item.output_format self.selected_format = queued_item.output_format
self.char_count = item.total_char_count self.char_count = queued_item.total_char_count
self.replace_single_newlines = getattr(item, "replace_single_newlines", False) self.replace_single_newlines = getattr(queued_item, "replace_single_newlines", False)
self.start_conversion() self.start_conversion(from_queue=True)
else: else:
# Queue finished, reset index # Queue finished, reset index
self.current_queue_index = 0 self.current_queue_index = 0
@@ -1542,7 +1578,7 @@ class abogen(QWidget):
def get_selected_lang(self, voice_formula) -> str: def get_selected_lang(self, voice_formula) -> str:
if self.selected_profile_name: 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, {}) entry = load_profiles().get(self.selected_profile_name, {})
selected_lang = entry.get("language") selected_lang = entry.get("language")
else: else:
@@ -1560,7 +1596,7 @@ class abogen(QWidget):
else self.subtitle_mode else self.subtitle_mode
) )
def start_conversion(self): def start_conversion(self, from_queue=False):
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.")
return return
@@ -1624,6 +1660,7 @@ class abogen(QWidget):
start_time=self.start_time, start_time=self.start_time,
total_char_count=self.char_count, total_char_count=self.char_count,
use_gpu=self.gpu_ok, use_gpu=self.gpu_ok,
from_queue=from_queue,
) # Use gpu_ok status ) # Use gpu_ok status
# Pass the displayed file path to the log_updated signal handler in ConversionThread # Pass the displayed file path to the log_updated signal handler in ConversionThread
self.conversion_thread.display_path = display_path self.conversion_thread.display_path = display_path
@@ -1695,11 +1732,13 @@ class abogen(QWidget):
if self.displayed_file_path if self.displayed_file_path
else self.selected_file else self.selected_file
) )
# Check if the file exists before trying to set file info # Only repopulate if not cleared by queue
if display_path and os.path.exists(display_path): if not getattr(self, 'input_box_cleared_by_queue', False):
self.input_box.set_file_info(display_path) if display_path and os.path.exists(display_path):
self.input_box.set_file_info(display_path)
else:
self.input_box.clear_input()
else: else:
# Clear the input if the file no longer exists
self.input_box.clear_input() self.input_box.clear_input()
return return
@@ -1738,8 +1777,6 @@ class abogen(QWidget):
def reset_ui(self): def reset_ui(self):
try: try:
self.restore_input_box()
self.input_box.clear_input() # Reset text and style
self.etr_label.hide() # Hide ETR label self.etr_label.hide() # Hide ETR label
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
self.progress_bar.hide() self.progress_bar.hide()
@@ -1762,6 +1799,8 @@ class abogen(QWidget):
pass # Ignore error if not connected pass # Ignore error if not connected
self.btn_start.clicked.connect(self.start_conversion) self.btn_start.clicked.connect(self.start_conversion)
self.enable_disable_queue_buttons() self.enable_disable_queue_buttons()
self.restore_input_box()
self.input_box.clear_input() # Reset text and style
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}")
@@ -1777,11 +1816,13 @@ class abogen(QWidget):
self.displayed_file_path if self.displayed_file_path else self.selected_file self.displayed_file_path if self.displayed_file_path else self.selected_file
) )
# Check if the file exists before trying to set file info # Only repopulate if not cleared by queue
if display_path and os.path.exists(display_path): if not getattr(self, 'input_box_cleared_by_queue', False):
self.input_box.set_file_info(display_path) if display_path and os.path.exists(display_path):
self.input_box.set_file_info(display_path)
else:
self.input_box.clear_input()
else: else:
# Clear the input if the file no longer exists
self.input_box.clear_input() self.input_box.clear_input()
# Ensure open file button is visible when going back # Ensure open file button is visible when going back
@@ -1868,7 +1909,7 @@ class abogen(QWidget):
voice_formula = " + ".join(filter(None, components)) voice_formula = " + ".join(filter(None, components))
voice_to_cache = voice_formula voice_to_cache = voice_formula
if self.selected_profile_name: 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, {}) entry = load_profiles().get(self.selected_profile_name, {})
lang_to_cache = entry.get("language") lang_to_cache = entry.get("language")
else: else:
@@ -2002,7 +2043,7 @@ class abogen(QWidget):
voice = " + ".join(filter(None, components)) voice = " + ".join(filter(None, components))
# determine language: use profile setting, else explicit mixer selection, else fallback to first voice code # determine language: use profile setting, else explicit mixer selection, else fallback to first voice code
if self.selected_profile_name: 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, {}) entry = load_profiles().get(self.selected_profile_name, {})
lang = entry.get("language") lang = entry.get("language")
@@ -2151,11 +2192,13 @@ class abogen(QWidget):
if self.displayed_file_path if self.displayed_file_path
else self.selected_file else self.selected_file
) )
# Check if the file exists before trying to set file info # Only repopulate if not cleared by queue
if display_path and os.path.exists(display_path): if not getattr(self, 'input_box_cleared_by_queue', False):
self.input_box.set_file_info(display_path) if display_path and os.path.exists(display_path):
self.input_box.set_file_info(display_path)
else:
self.input_box.clear_input()
else: else:
# Clear the input if the file no longer exists
self.input_box.clear_input() self.input_box.clear_input()
if ( if (
hasattr(self, "conversion_thread") hasattr(self, "conversion_thread")
@@ -2213,7 +2256,7 @@ class abogen(QWidget):
def show_chapter_options_dialog(self, chapter_count): 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""" """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 = ChapterOptionsDialog(chapter_count, parent=self)
dialog.setWindowModality(Qt.ApplicationModal) dialog.setWindowModality(Qt.ApplicationModal)
@@ -2458,7 +2501,7 @@ class abogen(QWidget):
def reveal_config_in_explorer(self): def reveal_config_in_explorer(self):
"""Open the configuration file location in file explorer.""" """Open the configuration file location in file explorer."""
from utils import get_user_config_path from abogen.utils import get_user_config_path
try: try:
config_path = get_user_config_path() config_path = get_user_config_path()
@@ -2490,7 +2533,7 @@ class abogen(QWidget):
"""Create a desktop shortcut to this program using PowerShell.""" """Create a desktop shortcut to this program using PowerShell."""
import sys import sys
from platformdirs import user_desktop_dir from platformdirs import user_desktop_dir
from utils import create_process from abogen.utils import create_process
try: try:
if platform.system() == "Windows": if platform.system() == "Windows":
@@ -2625,7 +2668,7 @@ Categories=AudioVideo;Audio;Utility;
save_config(self.config) save_config(self.config)
def show_voice_formula_dialog(self): def show_voice_formula_dialog(self):
from voice_profiles import load_profiles from abogen.voice_profiles import load_profiles
profiles = load_profiles() profiles = load_profiles()
initial_state = None initial_state = None
+3 -3
View File
@@ -7,9 +7,9 @@ from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
# Add the directory to Python path # Add the directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__))) sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
from gui import abogen from abogen.gui import abogen
from utils import get_resource_path from abogen.utils import get_resource_path
from constants import PROGRAM_NAME, VERSION from abogen.constants import PROGRAM_NAME, VERSION
# Set environment variables for AMD ROCm # Set environment variables for AMD ROCm
os.environ["MIOPEN_FIND_MODE"] = "FAST" os.environ["MIOPEN_FIND_MODE"] = "FAST"
+11 -15
View File
@@ -16,7 +16,7 @@ from PyQt5.QtWidgets import (
QSizePolicy, QSizePolicy,
) )
from PyQt5.QtCore import QFileInfo, Qt from PyQt5.QtCore import QFileInfo, Qt
from constants import COLORS from abogen.constants import COLORS
from copy import deepcopy from copy import deepcopy
from PyQt5.QtGui import QFontMetrics from PyQt5.QtGui import QFontMetrics
@@ -243,26 +243,23 @@ class QueueManager(QDialog):
items = self.listwidget.selectedItems() items = self.listwidget.selectedItems()
if not items: if not items:
return return
import os
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
display_names = [item.text() for item in items] # Remove by index to ensure correct mapping
to_remove = [] rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
for q in self.queue:
if os.path.basename(q.file_name) in display_names:
to_remove.append(q)
# Warn user if removing multiple files # Warn user if removing multiple files
if len(to_remove) > 1: if len(rows) > 1:
reply = QMessageBox.question( reply = QMessageBox.question(
self, self,
"Confirm Remove", "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.Yes | QMessageBox.No,
QMessageBox.No QMessageBox.No
) )
if reply != QMessageBox.Yes: if reply != QMessageBox.Yes:
return return
for item in to_remove: for row in rows:
self.queue.remove(item) if 0 <= row < len(self.queue):
del self.queue[row]
self.process_queue() self.process_queue()
self.update_button_states() self.update_button_states()
@@ -328,7 +325,7 @@ class QueueManager(QDialog):
return attrs return attrs
def add_files_from_paths(self, file_paths): 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 from PyQt5.QtWidgets import QMessageBox
import os import os
current_attrs = self.get_current_attributes() current_attrs = self.get_current_attributes()
@@ -378,9 +375,8 @@ class QueueManager(QDialog):
self.update_button_states() self.update_button_states()
def add_more_files(self): def add_more_files(self):
from PyQt5.QtWidgets import QFileDialog, QMessageBox from PyQt5.QtWidgets import QFileDialog
import os from abogen.utils import calculate_text_length # import the function
from utils import calculate_text_length # import the function
# Only allow .txt files # Only allow .txt files
files, _ = QFileDialog.getOpenFileNames(self, "Select .txt files", "", "Text Files (*.txt)") files, _ = QFileDialog.getOpenFileNames(self, "Select .txt files", "", "Text Files (*.txt)")
if not files: if not files:
+4 -4
View File
@@ -28,7 +28,7 @@ from PyQt5.QtWidgets import (
) )
from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize
from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtGui import QPixmap, QIcon
from constants import ( from abogen.constants import (
VOICES_INTERNAL, VOICES_INTERNAL,
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
LANGUAGE_DESCRIPTIONS, LANGUAGE_DESCRIPTIONS,
@@ -36,8 +36,8 @@ from constants import (
) )
import re import re
import platform import platform
from utils import get_resource_path from abogen.utils import get_resource_path
from voice_profiles import ( from abogen.voice_profiles import (
load_profiles, load_profiles,
save_profiles, save_profiles,
delete_profile, delete_profile,
@@ -1084,7 +1084,7 @@ class VoiceFormulaDialog(QDialog):
self, "Import Profiles", "", "JSON Files (*.json)" self, "Import Profiles", "", "JSON Files (*.json)"
) )
if path: 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 to read the file and count profiles
try: try:
+1 -2
View File
@@ -1,6 +1,5 @@
import re import re
from constants import VOICES_INTERNAL from abogen.constants import VOICES_INTERNAL
# Calls parsing and loads the voice to gpu or cpu # Calls parsing and loads the voice to gpu or cpu
def get_new_voice(pipeline, formula, use_gpu): def get_new_voice(pipeline, formula, use_gpu):
+1 -1
View File
@@ -1,6 +1,6 @@
import os import os
import json import json
from utils import get_user_config_path from abogen.utils import get_user_config_path
def _get_profiles_path(): def _get_profiles_path():
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 62 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 31 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 69 KiB