Various improvements and modifications in code and documentation
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
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`
|
## `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 @@
|
|||||||
1.0.9
|
1.1.0
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,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,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():
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 69 KiB |