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)
- 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.
+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.
## `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`
<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))
## `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`
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:
+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_NAME = "abogen"
+45 -4
View File
@@ -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
# 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 = ""
# 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,6 +982,11 @@ class ConversionThread(QThread):
genre_match = re.search(r"<<METADATA_GENRE:([^>]*)>>", text)
# Use display path or filename as fallback for title
# 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:
+103 -60
View File
@@ -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()
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
# 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:
# Clear the input if the file no longer exists
self.input_box.clear_input()
else:
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
# 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:
# Clear the input if the file no longer exists
self.input_box.clear_input()
else:
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
# 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:
# Clear the input if the file no longer exists
self.input_box.clear_input()
else:
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
+3 -3
View File
@@ -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"
+11 -15
View File
@@ -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:
+4 -4
View File
@@ -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:
+1 -2
View File
@@ -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):
+1 -1
View File
@@ -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():
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