10 Commits
13 changed files with 1221 additions and 701 deletions
+9
View File
@@ -1,3 +1,12 @@
# 1.2.1
- Upgraded Abogen's interface from PyQt5 to PyQt6 for better compatibility and long-term support.
- Added tooltip indicators in queue manager to display book handler options (`Save chapters separately` and `Merge chapters at the end`) for queued items.
- Added `Open processed file` and `Open input file` options for items in the queue manager, instead of just `Open file` option.
- Added loading gif animation to book handler window.
- Fixed light theme slider colors in voice mixer for better visibility (for non-Windows users).
- Fixed subtitle word-count splitting logic for more accurate segmentation.
- Improvements in code and documentation.
# 1.2.0
- Added `Line` option to subtitle generation modes, allowing subtitles to be generated based on line breaks in the text, by @mleg in #94.
- Added a loading indicator to the book handler window for better user experience during book preprocessing.
+1 -1
View File
@@ -12,7 +12,7 @@ RUN apt-get update \
python3 \
python3-venv \
python3-pip \
python3-pyqt5 \
python3-pyqt6 \
espeak-ng \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
+1 -1
View File
@@ -1 +1 @@
1.2.0
1.2.1
+343 -201
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -148,6 +148,7 @@ COLORS = {
"BLUE_BORDER_HOVER": "#6ab0de",
"YELLOW_BACKGROUND": "rgba(255, 221, 51, 0.40)",
"GREY_BACKGROUND": "rgba(128, 128, 128, 0.15)",
"GREY_BORDER": "#808080",
"RED_BACKGROUND": "rgba(232, 78, 60, 0.15)",
"RED_BG": "rgba(232, 78, 60, 0.10)",
"RED_BG_HOVER": "rgba(232, 78, 60, 0.15)",
+109 -44
View File
@@ -3,10 +3,15 @@ import re
import time
import hashlib # For generating unique cache filenames
from platformdirs import user_desktop_dir
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt6.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
import soundfile as sf
from abogen.utils import clean_text, create_process, get_user_cache_path, detect_encoding
from abogen.utils import (
clean_text,
create_process,
get_user_cache_path,
detect_encoding,
)
from abogen.constants import (
LANGUAGE_DESCRIPTIONS,
SAMPLE_VOICE_TEXTS,
@@ -47,43 +52,45 @@ def sanitize_name_for_os(name, is_folder=True):
if system == "Windows":
# Windows illegal characters: < > : " / \ | ? *
# Also can't end with space or dot
sanitized = re.sub(r'[<>:"/\\|?*]', '_', name)
sanitized = re.sub(r'[<>:"/\\|?*]', "_", name)
# Remove control characters (0-31)
sanitized = re.sub(r'[\x00-\x1f]', '_', sanitized)
sanitized = re.sub(r"[\x00-\x1f]", "_", sanitized)
# Remove trailing spaces and dots
sanitized = sanitized.rstrip('. ')
sanitized = sanitized.rstrip(". ")
# Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
reserved = ['CON', 'PRN', 'AUX', 'NUL'] + \
[f'COM{i}' for i in range(1, 10)] + \
[f'LPT{i}' for i in range(1, 10)]
if sanitized.upper() in reserved or sanitized.upper().split('.')[0] in reserved:
reserved = (
["CON", "PRN", "AUX", "NUL"]
+ [f"COM{i}" for i in range(1, 10)]
+ [f"LPT{i}" for i in range(1, 10)]
)
if sanitized.upper() in reserved or sanitized.upper().split(".")[0] in reserved:
sanitized = f"_{sanitized}"
elif system == "Darwin": # macOS
# macOS illegal characters: : (colon is converted to / by the system)
# Also can't start with dot (hidden file) for folders typically
sanitized = re.sub(r'[:]', '_', name)
sanitized = re.sub(r"[:]", "_", name)
# Remove control characters
sanitized = re.sub(r'[\x00-\x1f]', '_', sanitized)
sanitized = re.sub(r"[\x00-\x1f]", "_", sanitized)
# Avoid leading dot for folders (creates hidden folders)
if is_folder and sanitized.startswith('.'):
sanitized = '_' + sanitized[1:]
if is_folder and sanitized.startswith("."):
sanitized = "_" + sanitized[1:]
else: # Linux and others
# Linux illegal characters: / and null character
# Though / is illegal, most other chars are technically allowed
sanitized = re.sub(r'[/\x00]', '_', name)
sanitized = re.sub(r"[/\x00]", "_", name)
# Remove other control characters for safety
sanitized = re.sub(r'[\x01-\x1f]', '_', sanitized)
sanitized = re.sub(r"[\x01-\x1f]", "_", sanitized)
# Avoid leading dot for folders (creates hidden folders)
if is_folder and sanitized.startswith('.'):
sanitized = '_' + sanitized[1:]
if is_folder and sanitized.startswith("."):
sanitized = "_" + sanitized[1:]
# Ensure the name is not empty after sanitization
if not sanitized or sanitized.strip() == '':
if not sanitized or sanitized.strip() == "":
sanitized = "audiobook"
# Limit length to 255 characters (common limit across filesystems)
if len(sanitized) > 255:
sanitized = sanitized[:255].rstrip('. ')
sanitized = sanitized[:255].rstrip(". ")
return sanitized
@@ -96,8 +103,8 @@ class ChapterOptionsDialog(QDialog):
# Prevent closing with the X button and remove the help button
self.setWindowFlags(
self.windowFlags()
& ~Qt.WindowCloseButtonHint
& ~Qt.WindowContextHelpButtonHint
& ~Qt.WindowType.WindowCloseButtonHint
& ~Qt.WindowType.WindowContextHelpButtonHint
)
layout = QVBoxLayout(self)
@@ -131,7 +138,7 @@ class ChapterOptionsDialog(QDialog):
layout.addWidget(self.countdown_label)
# Add OK button
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
button_box.accepted.connect(self.accept)
layout.addWidget(button_box)
@@ -180,7 +187,7 @@ class ChapterOptionsDialog(QDialog):
# Prevent escape key from closing the dialog
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
if event.key() == Qt.Key.Key_Escape:
event.ignore()
else:
super().keyPressEvent(event)
@@ -310,7 +317,11 @@ class ConversionThread(QThread):
# Normalize paths for consistent display (fixes Windows path separator issues)
input_file = os.path.normpath(input_file) if input_file else input_file
processing_file = os.path.normpath(processing_file) if processing_file else processing_file
processing_file = (
os.path.normpath(processing_file)
if processing_file
else processing_file
)
self.log_updated.emit(f"- Input File: {input_file}")
if input_file != processing_file:
@@ -318,7 +329,9 @@ class ConversionThread(QThread):
# Use file_name for logs if from_queue, otherwise use display_path if available
if getattr(self, "from_queue", False):
base_path = self.save_base_path or self.file_name # Use save_base_path if available
base_path = (
self.save_base_path or self.file_name
) # Use save_base_path if available
else:
base_path = self.display_path if self.display_path else self.file_name
@@ -476,7 +489,9 @@ class ConversionThread(QThread):
# Use file_name for logs if from_queue, otherwise use display_path if available
if getattr(self, "from_queue", False):
base_path = self.save_base_path or self.file_name # Use save_base_path if available
base_path = (
self.save_base_path or self.file_name
) # Use save_base_path if available
else:
base_path = self.display_path if self.display_path else self.file_name
@@ -528,7 +543,9 @@ class ConversionThread(QThread):
# Prepare merged output file for incremental writing ONLY if merge_chapters_at_end is True
if merge_chapters_at_end:
out_dir = parent_dir
base_filepath_no_ext = os.path.join(out_dir, f"{sanitized_base_name}{suffix}")
base_filepath_no_ext = os.path.join(
out_dir, f"{sanitized_base_name}{suffix}"
)
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
subtitle_entries = []
current_time = 0.0
@@ -642,8 +659,12 @@ class ConversionThread(QThread):
# Add style definitions for karaoke highlighting
if self.subtitle_mode == "Sentence + Highlighting":
merged_subtitle_file.write("[V4+ Styles]\n")
merged_subtitle_file.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n")
merged_subtitle_file.write("Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n")
merged_subtitle_file.write(
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
)
merged_subtitle_file.write(
"Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n"
)
merged_subtitle_file.write("[Events]\n")
merged_subtitle_file.write(
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
@@ -806,8 +827,12 @@ class ConversionThread(QThread):
# Add style definitions for karaoke highlighting
if self.subtitle_mode == "Sentence + Highlighting":
chapter_subtitle_file.write("[V4+ Styles]\n")
chapter_subtitle_file.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n")
chapter_subtitle_file.write("Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n")
chapter_subtitle_file.write(
"Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
)
chapter_subtitle_file.write(
"Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n"
)
chapter_subtitle_file.write("[Events]\n")
chapter_subtitle_file.write(
@@ -922,7 +947,12 @@ class ConversionThread(QThread):
start_time = self._ass_time(start)
end_time = self._ass_time(end)
# Use karaoke effect for highlighting mode
effect = "karaoke" if self.subtitle_mode == "Sentence + Highlighting" else ""
effect = (
"karaoke"
if self.subtitle_mode
== "Sentence + Highlighting"
else ""
)
merged_subtitle_file.write(
f"Dialogue: 0,{start_time},{end_time},Default,,{merged_subtitle_margin},{merged_subtitle_margin},0,{effect},{merged_subtitle_alignment_tag}{text}\n"
)
@@ -951,7 +981,12 @@ class ConversionThread(QThread):
start_time = self._ass_time(start)
end_time = self._ass_time(end)
# Use karaoke effect for highlighting mode
effect = "karaoke" if self.subtitle_mode == "Sentence + Highlighting" else ""
effect = (
"karaoke"
if self.subtitle_mode
== "Sentence + Highlighting"
else ""
)
chapter_subtitle_file.write(
f"Dialogue: 0,{start_time},{end_time},Default,,{chapter_subtitle_margin},{chapter_subtitle_margin},0,{effect},{chapter_subtitle_alignment_tag}{text}\n"
)
@@ -997,7 +1032,9 @@ class ConversionThread(QThread):
# Add silence between chapters for merged output (except after the last chapter)
if merge_chapters_at_end and chapter_idx < total_chapters:
silence_samples = int(self.silence_duration * 24000) # Silence duration at 24,000 Hz
silence_samples = int(
self.silence_duration * 24000
) # Silence duration at 24,000 Hz
silence_audio = self.np.zeros(silence_samples, dtype="float32")
silence_bytes = silence_audio.tobytes()
@@ -1297,7 +1334,11 @@ class ConversionThread(QThread):
karaoke_text = ""
for t in current_sentence:
# Calculate duration in centiseconds
duration = t["end"] - t["start"] if t["end"] and t["start"] else 0.5
duration = (
t["end"] - t["start"]
if t["end"] and t["start"]
else 0.5
)
duration_cs = int(duration * 100)
# Add karaoke effect - relies on style's SecondaryColour for highlighting
karaoke_text += f"{{\\kf{duration_cs}}}{t['text']}{t.get('whitespace', '') or ''}"
@@ -1382,23 +1423,48 @@ class ConversionThread(QThread):
subtitle_entries[-1] = (start, fallback_end_time, text)
else:
# Word count-based grouping
# Word count-based grouping - simply count spaces and split after N spaces
try:
word_count = int(self.subtitle_mode.split()[0])
word_count = min(word_count, max_subtitle_words)
except (ValueError, IndexError):
word_count = 1
# Group words into subtitle entries (processed_tokens already has punctuation combined)
for i in range(0, len(processed_tokens), word_count):
group = processed_tokens[i : i + word_count]
if group:
current_group = []
space_count = 0
for token in processed_tokens:
current_group.append(token)
# Count spaces after tokens (in the whitespace field)
if token.get("whitespace", "") == " ":
space_count += 1
# Split after counting N spaces
if space_count >= word_count:
text = "".join(
t["text"] + (t.get("whitespace", "") or "") for t in group
t["text"] + (t.get("whitespace", "") or "")
for t in current_group
)
subtitle_entries.append(
(group[0]["start"], group[-1]["end"], text.strip())
(
current_group[0]["start"],
current_group[-1]["end"],
text.strip(),
)
)
current_group = []
space_count = 0
# Add any remaining tokens
if current_group:
text = "".join(
t["text"] + (t.get("whitespace", "") or "") for t in current_group
)
subtitle_entries.append(
(current_group[0]["start"], current_group[-1]["end"], text.strip())
)
# Fallback for last entry
if subtitle_entries and fallback_end_time is not None:
last_entry = subtitle_entries[-1]
@@ -1406,7 +1472,6 @@ class ConversionThread(QThread):
if end is None or end <= start or end <= 0:
subtitle_entries[-1] = (start, fallback_end_time, text)
def cancel(self):
self.cancel_requested = True
self.should_cancel = True
+210 -118
View File
@@ -8,28 +8,30 @@ from abogen.queue_manager_gui import QueueManager
from abogen.queued_item import QueuedItem
import abogen.hf_tracker as hf_tracker
import hashlib # Added for cache path generation
from PyQt5.QtWidgets import (
from PyQt6.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QFileDialog,
QLabel,
QProgressBar,
QSlider,
QComboBox,
QSizePolicy,
QTextEdit,
QFileIconProvider,
QLabel,
QSlider,
QMessageBox,
QFileDialog,
QProgressBar,
QFrame,
QStyleFactory,
QInputDialog,
QFileIconProvider,
QSizePolicy,
QDialog,
QCheckBox,
QMenu,
QAction,
QActionGroup,
)
from PyQt5.QtCore import (
from PyQt6.QtGui import QAction, QActionGroup
from PyQt6.QtCore import (
Qt,
QUrl,
QPoint,
@@ -42,8 +44,9 @@ from PyQt5.QtCore import (
QSize,
QTimer,
QEvent,
QProcess,
)
from PyQt5.QtGui import (
from PyQt6.QtGui import (
QTextCursor,
QDesktopServices,
QIcon,
@@ -52,6 +55,7 @@ from PyQt5.QtGui import (
QPolygon,
QColor,
QMovie,
QPalette,
)
from abogen.utils import (
load_config,
@@ -95,12 +99,12 @@ class DarkTitleBarEventFilter(QObject):
self.set_title_bar_dark_mode = set_title_bar_dark_mode_func
def eventFilter(self, obj, event):
if event.type() == QEvent.Show:
if event.type() == QEvent.Type.Show:
# Only apply to QWidget windows
if isinstance(obj, QWidget) and obj.isWindow():
if self.is_windows and self.get_dark_mode():
self.set_title_bar_dark_mode(obj, True)
return False
return super().eventFilter(obj, event)
class ShowWarningSignalEmitter(QObject): # New class to handle signal emission
@@ -154,7 +158,7 @@ class InputBox(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setAlignment(Qt.AlignCenter)
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.setAcceptDrops(True)
self.setText(
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)"
@@ -162,8 +166,8 @@ class InputBox(QLabel):
self.setStyleSheet(
f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setCursor(Qt.PointingHandCursor)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.setCursor(Qt.CursorShape.PointingHandCursor)
# Add clear button
self.clear_btn = QPushButton("", self)
@@ -223,7 +227,7 @@ class InputBox(QLabel):
pixmap = qicon.pixmap(size)
# convert to base64 PNG
buffer = QBuffer()
buffer.open(QIODevice.WriteOnly)
buffer.open(QIODevice.OpenModeFlag.WriteOnly)
pixmap.save(buffer, "PNG")
img_data = base64.b64encode(buffer.data()).decode()
@@ -275,7 +279,9 @@ class InputBox(QLabel):
char_count = cached_char_count
elif char_source_path:
try:
with open(char_source_path, "r", encoding="utf-8", errors="ignore") as f:
with open(
char_source_path, "r", encoding="utf-8", errors="ignore"
) as f:
text = f.read()
cleaned_text = clean_text(text)
char_count = calculate_text_length(cleaned_text)
@@ -323,7 +329,8 @@ class InputBox(QLabel):
# For epub/pdf files, show edit if we have a selected_file (temp txt)
if (
window.selected_file_type in ["epub", "pdf", "md", "markdown", "md", "markdown"]
window.selected_file_type
in ["epub", "pdf", "md", "markdown", "md", "markdown"]
and window.selected_file
):
should_show_edit = True
@@ -356,6 +363,9 @@ class InputBox(QLabel):
self.window().displayed_file_path = (
None # Reset the displayed file path when clearing input
)
# Reset book handler attributes
self.window().save_chapters_separately = None
self.window().merge_chapters_at_end = None
self.setText(
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf, .md)"
)
@@ -384,7 +394,7 @@ class InputBox(QLabel):
return f"{size:.{decimal_places}f} PB"
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
if event.button() == Qt.MouseButton.LeftButton:
self.window().open_file_dialog()
def dragEnterEvent(self, event):
@@ -446,9 +456,11 @@ class InputBox(QLabel):
)
self.set_file_info(file_path)
event.acceptProposedAction()
elif (file_path.lower().endswith(".epub")
elif (
file_path.lower().endswith(".epub")
or file_path.lower().endswith(".pdf")
or file_path.lower().endswith((".md", ".markdown"))):
or file_path.lower().endswith((".md", ".markdown"))
):
# Determine file type
if file_path.lower().endswith(".epub"):
file_type = "epub"
@@ -472,7 +484,10 @@ class InputBox(QLabel):
def on_chapters_clicked(self):
win = self.window()
if win.selected_file_type in ["epub", "pdf", "md", "markdown"] and win.selected_book_path:
if (
win.selected_file_type in ["epub", "pdf", "md", "markdown"]
and win.selected_book_path
):
# Call open_book_file which shows the dialog and updates selected_chapters
if win.open_book_file(win.selected_book_path):
# Refresh the info label and button text after dialog closes
@@ -484,7 +499,10 @@ class InputBox(QLabel):
def on_edit_clicked(self):
win = self.window()
# For PDFs and EPUBs, use the temporary text file
if win.selected_file_type in ["epub", "pdf", "md", "markdown"] and win.selected_file:
if (
win.selected_file_type in ["epub", "pdf", "md", "markdown"]
and win.selected_file
):
# Use the temporary .txt file that was generated
win.open_textbox_dialog(win.selected_file)
else:
@@ -512,9 +530,16 @@ class InputBox(QLabel):
and cache_dir
):
# Consider it cached when the file is under the cache directory and is a .txt
if file_to_check.endswith('.txt') and os.path.commonpath([os.path.abspath(file_to_check), os.path.abspath(cache_dir)]) == os.path.abspath(cache_dir):
if file_to_check.endswith(".txt") and os.path.commonpath(
[os.path.abspath(file_to_check), os.path.abspath(cache_dir)]
) == os.path.abspath(cache_dir):
# Only treat as document-cache when original type was a document
if getattr(win, 'selected_file_type', None) in ['epub', 'pdf', 'md', 'markdown']:
if getattr(win, "selected_file_type", None) in [
"epub",
"pdf",
"md",
"markdown",
]:
is_cached_doc = True
if is_cached_doc:
@@ -530,8 +555,11 @@ class InputBox(QLabel):
act_input = QAction("Go to input file", self)
# Prefer displayed_file_path (original input path) then selected_book_path
input_path = getattr(win, 'displayed_file_path', None) or getattr(win, 'selected_book_path', None)
input_path = getattr(win, "displayed_file_path", None) or getattr(
win, "selected_book_path", None
)
if input_path and os.path.exists(input_path):
def open_input():
folder_path = os.path.dirname(input_path)
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
@@ -542,7 +570,11 @@ class InputBox(QLabel):
menu.addAction(act_input)
# Show the menu anchored to the button
menu.exec_(self.go_to_folder_btn.mapToGlobal(QPoint(0, self.go_to_folder_btn.height())))
menu.exec(
self.go_to_folder_btn.mapToGlobal(
QPoint(0, self.go_to_folder_btn.height())
)
)
else:
if (
file_to_check
@@ -560,7 +592,9 @@ class TextboxDialog(QDialog):
super().__init__(parent)
self.setWindowTitle("Enter Text")
self.setWindowFlags(
Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint
Qt.WindowType.Window
| Qt.WindowType.WindowCloseButtonHint
| Qt.WindowType.WindowMaximizeButtonHint
)
self.resize(700, 500)
@@ -634,16 +668,18 @@ class TextboxDialog(QDialog):
# Check if we need to warn about overwriting a non-temporary file
if hasattr(self, "is_non_cache_file") and self.is_non_cache_file:
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Warning)
msg_box.setIcon(QMessageBox.Icon.Warning)
msg_box.setWindowTitle("File Overwrite Warning")
msg_box.setText(
f"You are about to overwrite the original file:\n{self.non_cache_file_path}"
)
msg_box.setInformativeText("Do you want to continue?")
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg_box.setDefaultButton(QMessageBox.No)
msg_box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msg_box.setDefaultButton(QMessageBox.StandardButton.No)
if msg_box.exec_() != QMessageBox.Yes:
if msg_box.exec() != QMessageBox.StandardButton.Yes:
# User canceled, don't close the dialog
return
@@ -815,6 +851,7 @@ class abogen(QWidget):
# Enable/disable subtitle options based on selected language (profile or voice)
enable = self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
self.subtitle_combo.setEnabled(enable)
self.subtitle_format_combo.setEnabled(enable)
# loading gif for preview button
loading_gif_path = get_resource_path("abogen.assets", "loading.gif")
if loading_gif_path:
@@ -873,7 +910,7 @@ class abogen(QWidget):
self.log_text = QTextEdit(self)
self.log_text.setReadOnly(True)
self.log_text.setUndoRedoEnabled(False)
self.log_text.setFrameStyle(QTextEdit.NoFrame)
self.log_text.setFrameStyle(QFrame.Shape.NoFrame)
self.log_text.setStyleSheet("QTextEdit { border: none; }")
self.log_text.hide()
container_layout.addWidget(self.log_text, 1)
@@ -884,11 +921,11 @@ class abogen(QWidget):
speed_layout = QVBoxLayout()
speed_layout.setSpacing(2)
speed_layout.addWidget(QLabel("Speed:", self))
self.speed_slider = QSlider(Qt.Horizontal, self)
self.speed_slider = QSlider(Qt.Orientation.Horizontal, self)
self.speed_slider.setMinimum(10)
self.speed_slider.setMaximum(200)
self.speed_slider.setValue(100)
self.speed_slider.setTickPosition(QSlider.TicksBelow)
self.speed_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
self.speed_slider.setTickInterval(5)
self.speed_slider.setSingleStep(5)
speed_layout.addWidget(self.speed_slider)
@@ -910,7 +947,9 @@ class abogen(QWidget):
"The first character represents the language:\n"
'"a" => American English\n"b" => British English\n"e" => Spanish\n"f" => French\n"h" => Hindi\n"i" => Italian\n"j" => Japanese\n"p" => Brazilian Portuguese\n"z" => Mandarin Chinese\nThe second character represents the gender:\n"m" => Male\n"f" => Female'
)
self.voice_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.voice_combo.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
voice_layout.addWidget(self.voice_combo)
# Voice formula button
self.btn_voice_formula_mixer = QPushButton(self)
@@ -925,11 +964,11 @@ class abogen(QWidget):
# Play/Stop icons
def make_icon(color, shape):
pix = QPixmap(20, 20)
pix.fill(Qt.transparent)
pix.fill(Qt.GlobalColor.transparent)
p = QPainter(pix)
p.setRenderHint(QPainter.Antialiasing)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.setBrush(QColor(*color))
p.setPen(Qt.NoPen)
p.setPen(Qt.PenStyle.NoPen)
if shape == "play":
pts = [
pix.rect().topLeft() + QPoint(4, 2),
@@ -976,14 +1015,20 @@ class abogen(QWidget):
for lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
)
subtitle_options = ["Disabled", "Line", "Sentence", "Sentence + Comma", "Sentence + Highlighting"] + [
f"{i} word" if i == 1 else f"{i} words" for i in range(1, 11)
]
subtitle_options = [
"Disabled",
"Line",
"Sentence",
"Sentence + Comma",
"Sentence + Highlighting",
] + [f"{i} word" if i == 1 else f"{i} words" for i in range(1, 11)]
self.subtitle_combo.addItems(subtitle_options)
self.subtitle_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }"
)
self.subtitle_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.subtitle_combo.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.subtitle_combo.setCurrentText(self.subtitle_mode)
self.subtitle_combo.currentTextChanged.connect(self.on_subtitle_mode_changed)
# Enable/disable subtitle options based on selected language (profile or voice)
@@ -1001,7 +1046,9 @@ class abogen(QWidget):
self.format_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }"
)
self.format_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.format_combo.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
# Add items with display labels and underlying keys
for key, label in [
("wav", "wav"),
@@ -1032,7 +1079,7 @@ class abogen(QWidget):
"QComboBox { min-height: 20px; padding: 6px 12px; }"
)
self.subtitle_format_combo.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
for value, text in SUBTITLE_FORMATS:
self.subtitle_format_combo.addItem(text, value)
@@ -1047,7 +1094,10 @@ class abogen(QWidget):
# If subtitle mode requires highlighting, SRT is not supported. Disable SRT item
# and auto-switch to a compatible ASS format if SRT is currently selected.
try:
if hasattr(self, "subtitle_mode") and self.subtitle_mode == "Sentence + Highlighting":
if (
hasattr(self, "subtitle_mode")
and self.subtitle_mode == "Sentence + Highlighting"
):
idx_srt = self.subtitle_format_combo.findData("srt")
if idx_srt >= 0:
item = self.subtitle_format_combo.model().item(idx_srt)
@@ -1059,10 +1109,16 @@ class abogen(QWidget):
if new_idx >= 0:
self.subtitle_format_combo.setCurrentIndex(new_idx)
# Persist the change
self.set_subtitle_format(self.subtitle_format_combo.itemData(new_idx))
self.set_subtitle_format(
self.subtitle_format_combo.itemData(new_idx)
)
except Exception:
# Fail-safe: don't crash UI if model manipulation isn't supported on some platforms
pass
# Enable/disable subtitle format based on selected language
self.subtitle_format_combo.setEnabled(
self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
controls_layout.addLayout(subtitle_format_layout)
# Replace single newlines dropdown (acts like checkbox)
@@ -1079,7 +1135,7 @@ class abogen(QWidget):
"QComboBox { min-height: 20px; padding: 6px 12px; }"
)
self.replace_newlines_combo.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
# Set initial value based on config
self.replace_newlines_combo.setCurrentIndex(
@@ -1106,7 +1162,9 @@ class abogen(QWidget):
self.save_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }"
)
self.save_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.save_combo.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.save_combo.setCurrentText(self.save_option)
self.save_combo.currentTextChanged.connect(self.on_save_option_changed)
save_layout.addWidget(self.save_combo)
@@ -1121,7 +1179,9 @@ class abogen(QWidget):
save_path_row.addWidget(selected_folder_label)
self.save_path_label = QLabel("", self.save_path_row_widget)
self.save_path_label.setStyleSheet(f"QLabel {{ color: {COLORS['GREEN']}; }}")
self.save_path_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.save_path_label.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
)
save_path_row.addWidget(self.save_path_label)
self.save_path_row_widget.hide() # Hide the whole row by default
controls_layout.addWidget(self.save_path_row_widget)
@@ -1164,9 +1224,12 @@ class abogen(QWidget):
self.btn_start.setFixedHeight(60)
self.btn_start.clicked.connect(self.start_conversion)
controls_layout.addWidget(self.btn_start)
# Add controls to a container widget
self.controls_widget = QWidget()
self.controls_widget.setLayout(controls_layout)
self.controls_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.controls_widget.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed
)
container_layout.addWidget(self.controls_widget)
# Progress bar
self.progress_bar = QProgressBar(self)
@@ -1175,7 +1238,7 @@ class abogen(QWidget):
container_layout.addWidget(self.progress_bar)
# ETR Label
self.etr_label = QLabel("Estimated time remaining: Calculating...", self)
self.etr_label.setAlignment(Qt.AlignCenter)
self.etr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.etr_label.hide()
container_layout.addWidget(self.etr_label)
# Cancel button
@@ -1232,9 +1295,11 @@ class abogen(QWidget):
)
if not file_path:
return
if (file_path.lower().endswith(".epub")
if (
file_path.lower().endswith(".epub")
or file_path.lower().endswith(".pdf")
or file_path.lower().endswith((".md", ".markdown"))):
or file_path.lower().endswith((".md", ".markdown"))
):
# Determine file type
if file_path.lower().endswith(".epub"):
self.selected_file_type = "epub"
@@ -1270,16 +1335,16 @@ class abogen(QWidget):
# HandlerDialog uses internal caching to avoid reprocessing the same book
dialog = HandlerDialog(
book_path,
file_type=getattr(self, 'selected_file_type', None),
file_type=getattr(self, "selected_file_type", None),
checked_chapters=self.selected_chapters,
parent=self
parent=self,
)
dialog.setWindowModality(Qt.NonModal)
dialog.setWindowModality(Qt.WindowModality.NonModal)
dialog.setModal(False)
dialog.show() # We'll handle the dialog result asynchronously
def on_dialog_finished(result):
if result != QDialog.Accepted:
if result != QDialog.DialogCode.Accepted:
return False
chapters_text, all_checked_hrefs = dialog.get_selected_text()
if not all_checked_hrefs:
@@ -1319,7 +1384,7 @@ class abogen(QWidget):
if self.save_as_project:
# Get project directory from user
project_dir = QFileDialog.getExistingDirectory(
self, "Select Project Folder", "", QFileDialog.ShowDirsOnly
self, "Select Project Folder", "", QFileDialog.Option.ShowDirsOnly
)
if not project_dir:
# User cancelled, fallback to cache
@@ -1445,7 +1510,7 @@ class abogen(QWidget):
dialog.non_cache_file_path = edit_file
except Exception:
pass
if dialog.exec_() == QDialog.Accepted:
if dialog.exec() == QDialog.DialogCode.Accepted:
text = dialog.get_text()
if not text.strip():
self._show_error_message_box("Textbox Error", "Text cannot be empty.")
@@ -1494,9 +1559,11 @@ class abogen(QWidget):
# Enable/disable subtitle options based on language
if self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION:
self.subtitle_combo.setEnabled(True)
self.subtitle_format_combo.setEnabled(True)
self.subtitle_mode = self.subtitle_combo.currentText()
else:
self.subtitle_combo.setEnabled(False)
self.subtitle_format_combo.setEnabled(False)
def on_voice_combo_changed(self, index):
data = self.voice_combo.itemData(index)
@@ -1521,6 +1588,9 @@ class abogen(QWidget):
self.subtitle_combo.setEnabled(
self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
self.subtitle_format_combo.setEnabled(
self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
else:
self.mixed_voice_state = None
self.selected_profile_name = None
@@ -1531,9 +1601,11 @@ class abogen(QWidget):
save_config(self.config)
if self.selected_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION:
self.subtitle_combo.setEnabled(True)
self.subtitle_format_combo.setEnabled(True)
self.subtitle_mode = self.subtitle_combo.currentText()
else:
self.subtitle_combo.setEnabled(False)
self.subtitle_format_combo.setEnabled(False)
def update_subtitle_combo_for_profile(self, profile_name):
from abogen.voice_profiles import load_profiles
@@ -1542,6 +1614,7 @@ class abogen(QWidget):
lang = entry.get("language") if isinstance(entry, dict) else None
enable = lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
self.subtitle_combo.setEnabled(enable)
self.subtitle_format_combo.setEnabled(enable)
def populate_profiles_in_voice_combo(self):
# preserve current voice or profile
@@ -1605,7 +1678,7 @@ class abogen(QWidget):
at_bottom = sb.value() == sb.maximum()
cursor = txt.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.movePosition(QTextCursor.MoveOperation.End)
fmt = cursor.charFormat()
if isinstance(message, tuple):
@@ -1624,7 +1697,7 @@ class abogen(QWidget):
end = doc.findBlockByNumber(excess).position()
trim_cursor = QTextCursor(doc)
trim_cursor.setPosition(start)
trim_cursor.setPosition(end, QTextCursor.KeepAnchor)
trim_cursor.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
trim_cursor.removeSelectedText()
if at_bottom:
@@ -1715,7 +1788,9 @@ class abogen(QWidget):
if self.selected_file_type in ["epub", "pdf", "md", "markdown"]:
file_to_queue = self.selected_file
# Use the original file path for save location
save_base_path = self.displayed_file_path if self.displayed_file_path else file_to_queue
save_base_path = (
self.displayed_file_path if self.displayed_file_path else file_to_queue
)
else:
file_to_queue = (
self.displayed_file_path
@@ -1743,6 +1818,8 @@ class abogen(QWidget):
total_char_count=self.char_count,
replace_single_newlines=self.replace_single_newlines,
save_base_path=save_base_path,
save_chapters_separately=getattr(self, "save_chapters_separately", None),
merge_chapters_at_end=getattr(self, "merge_chapters_at_end", None),
)
# Prevent adding duplicate items to the queue
@@ -1760,6 +1837,10 @@ class abogen(QWidget):
== item_queue.replace_single_newlines
and getattr(queued_item, "save_base_path", None)
== item_queue.save_base_path
and getattr(queued_item, "save_chapters_separately", None)
== item_queue.save_chapters_separately
and getattr(queued_item, "merge_chapters_at_end", None)
== item_queue.merge_chapters_at_end
):
QMessageBox.warning(
self, "Duplicate Item", "This item is already in the queue."
@@ -1779,10 +1860,10 @@ class abogen(QWidget):
self,
"Confirm Clear Queue",
f"Are you sure you want to clear {len(self.queued_items)} items from the queue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.Yes:
if reply != QMessageBox.StandardButton.Yes:
return
self.queued_items = []
self.enable_disable_queue_buttons()
@@ -1790,7 +1871,7 @@ class abogen(QWidget):
def manage_queue(self):
# show a dialog to manage the queue
dialog = QueueManager(self, self.queued_items)
if dialog.exec_() == QDialog.Accepted:
if dialog.exec() == QDialog.DialogCode.Accepted:
self.queued_items = dialog.get_queue()
# re-enable/disable buttons based on queue state
self.enable_disable_queue_buttons()
@@ -1820,7 +1901,9 @@ class abogen(QWidget):
queued_item, "replace_single_newlines", False
)
# Restore the original file path for save location
self.displayed_file_path = queued_item.save_base_path or queued_item.file_name
self.displayed_file_path = (
queued_item.save_base_path or queued_item.file_name
)
self.start_conversion(from_queue=True)
else:
# Queue finished, reset index
@@ -2057,7 +2140,7 @@ class abogen(QWidget):
dialog.setLayout(layout)
dialog.setMinimumSize(400, 300)
dialog.setSizeGripEnabled(True) # Allow resizing
dialog.exec_()
dialog.exec()
def on_conversion_finished(self, message, output_path):
prevent_sleep_end()
@@ -2516,14 +2599,14 @@ class abogen(QWidget):
def _show_error_message_box(self, title, message):
box = QMessageBox(self)
box.setIcon(QMessageBox.Critical)
box.setIcon(QMessageBox.Icon.Critical)
box.setWindowTitle(title)
box.setText(message)
copy_btn = QPushButton("Copy")
box.addButton(copy_btn, QMessageBox.ActionRole)
box.addButton(QMessageBox.Ok)
box.addButton(copy_btn, QMessageBox.ButtonRole.ActionRole)
box.addButton(QMessageBox.StandardButton.Ok)
copy_btn.clicked.connect(lambda: QApplication.clipboard().setText(message))
box.exec_()
box.exec()
def _show_preview_error_box(self, msg):
self._show_error_message_box("Preview Error", f"Preview error: {msg}")
@@ -2551,14 +2634,16 @@ class abogen(QWidget):
def cancel_conversion(self):
if self.is_converting:
box = QMessageBox(self)
box.setIcon(QMessageBox.Warning)
box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle("Cancel Conversion")
box.setText(
"A conversion is currently running. Are you sure you want to cancel?"
)
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
box.setDefaultButton(QMessageBox.No)
if box.exec_() != QMessageBox.Yes:
box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
box.setDefaultButton(QMessageBox.StandardButton.No)
if box.exec() != QMessageBox.StandardButton.Yes:
return
try:
if (
@@ -2626,7 +2711,9 @@ class abogen(QWidget):
new_idx = self.subtitle_format_combo.findData("ass_centered_narrow")
if new_idx >= 0:
self.subtitle_format_combo.setCurrentIndex(new_idx)
self.set_subtitle_format(self.subtitle_format_combo.itemData(new_idx))
self.set_subtitle_format(
self.subtitle_format_combo.itemData(new_idx)
)
else:
# Re-enable SRT option when not in highlighting mode
if idx_srt >= 0:
@@ -2643,7 +2730,7 @@ class abogen(QWidget):
save_config(self.config)
def on_gpu_setting_changed(self, state):
self.use_gpu = state == Qt.Checked
self.use_gpu = state == Qt.CheckState.Checked.value
self.config["use_gpu"] = self.use_gpu
save_config(self.config)
@@ -2660,14 +2747,16 @@ class abogen(QWidget):
def closeEvent(self, event):
if self.is_converting:
box = QMessageBox(self)
box.setIcon(QMessageBox.Warning)
box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle("Conversion in Progress")
box.setText(
"A conversion is currently running. Are you sure you want to exit?"
)
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
box.setDefaultButton(QMessageBox.No)
if box.exec_() == QMessageBox.Yes:
box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
box.setDefaultButton(QMessageBox.StandardButton.No)
if box.exec() == QMessageBox.StandardButton.Yes:
self.cleanup_conversion_thread()
event.accept()
else:
@@ -2681,10 +2770,10 @@ class abogen(QWidget):
from abogen.conversion import ChapterOptionsDialog
dialog = ChapterOptionsDialog(chapter_count, parent=self)
dialog.setWindowModality(Qt.ApplicationModal)
dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
# If dialog is accepted, pass the options to the conversion thread
if dialog.exec_() == QDialog.Accepted:
if dialog.exec() == QDialog.DialogCode.Accepted:
options = dialog.get_options()
if (
hasattr(self, "conversion_thread")
@@ -2696,8 +2785,6 @@ class abogen(QWidget):
self.cancel_conversion()
def apply_theme(self, theme):
from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtWidgets import QStyleFactory
app = QApplication.instance()
is_windows = platform.system() == "Windows"
@@ -2981,7 +3068,7 @@ class abogen(QWidget):
about_action.triggered.connect(self.show_about_dialog)
menu.addAction(about_action)
menu.exec_(self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height())))
menu.exec(self.settings_btn.mapToGlobal(QPoint(0, self.settings_btn.height())))
def toggle_replace_single_newlines(self, enabled):
self.replace_single_newlines = enabled
@@ -2989,7 +3076,7 @@ class abogen(QWidget):
save_config(self.config)
def restart_app(self):
from PyQt5.QtCore import QProcess
import sys
exe = sys.executable
@@ -3022,10 +3109,10 @@ class abogen(QWidget):
self,
"Restart Required",
message,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.Yes:
if reply == QMessageBox.StandardButton.Yes:
self.config["disable_kokoro_internet"] = disabled
save_config(self.config)
try:
@@ -3040,10 +3127,10 @@ class abogen(QWidget):
self,
"Reset Settings",
"This will reset all settings to their default values and restart the application.\n\nDo you want to continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.Yes:
if reply == QMessageBox.StandardButton.Yes:
from abogen.utils import get_user_config_path
config_path = get_user_config_path()
@@ -3204,9 +3291,9 @@ Categories=AudioVideo;Audio;Utility;
self,
"Install Application Entry",
"Install application entry for current user?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.Yes:
if reply == QMessageBox.StandardButton.Yes:
import shutil
user_app_dir = os.path.expanduser("~/.local/share/applications")
@@ -3266,12 +3353,11 @@ Categories=AudioVideo;Audio;Utility;
initial_state = entry.get("voices", [])
else:
initial_state = entry
else:
initial_state = []
self.selected_lang = entry[0][0] if entry and entry[0] else None
dialog = VoiceFormulaDialog(
self, initial_state=initial_state, selected_profile=selected_profile
)
if dialog.exec_() == QDialog.Accepted:
if dialog.exec() == QDialog.DialogCode.Accepted:
if dialog.current_profile:
self.selected_profile_name = dialog.current_profile
self.config["selected_profile_name"] = dialog.current_profile
@@ -3292,7 +3378,9 @@ Categories=AudioVideo;Audio;Utility;
# Create custom dialog
dialog = QDialog(self)
dialog.setWindowTitle(f"About {PROGRAM_NAME}")
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
dialog.setWindowFlags(
dialog.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint
)
dialog.setFixedSize(400, 320) # Increased height for new button
layout = QVBoxLayout(dialog)
@@ -3314,7 +3402,7 @@ Categories=AudioVideo;Audio;Utility;
title_label = QLabel(
f"<h1 style='margin-bottom: 0;'>{PROGRAM_NAME} <span style='font-size: 12px; font-weight: normal; color: #666;'>v{VERSION}</span></h1><h3 style='margin-top: 5px;'>Audiobook Generator</h3>"
)
title_label.setTextFormat(Qt.RichText)
title_label.setTextFormat(Qt.TextFormat.RichText)
header_layout.addWidget(title_label, 1)
layout.addLayout(header_layout)
@@ -3323,7 +3411,7 @@ Categories=AudioVideo;Audio;Utility;
f"<p>{PROGRAM_DESCRIPTION}</p>"
"<p>Visit the GitHub repository for updates, documentation, and to report issues.</p>"
)
desc_label.setTextFormat(Qt.RichText)
desc_label.setTextFormat(Qt.TextFormat.RichText)
desc_label.setWordWrap(True)
layout.addWidget(desc_label)
@@ -3346,7 +3434,7 @@ Categories=AudioVideo;Audio;Utility;
close_btn.setFixedHeight(32)
layout.addWidget(close_btn)
dialog.exec_()
dialog.exec()
def manual_check_for_updates(self):
"""Manually check for updates and always show result"""
@@ -3359,7 +3447,7 @@ Categories=AudioVideo;Audio;Utility;
def show_update_message(remote_version, local_version):
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Information)
msg_box.setIcon(QMessageBox.Icon.Information)
msg_box.setWindowTitle("Update Available")
msg_box.setText(
f"A new version of {PROGRAM_NAME} is available! ({local_version} > {remote_version})"
@@ -3371,9 +3459,11 @@ Categories=AudioVideo;Audio;Utility;
"Alternatively, visit the GitHub repository for more information. "
"Would you like to view the changelog?"
)
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg_box.setDefaultButton(QMessageBox.Yes)
if msg_box.exec_() == QMessageBox.Yes:
msg_box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
if msg_box.exec() == QMessageBox.StandardButton.Yes:
try:
QDesktopServices.openUrl(QUrl(GITHUB_URL + "/releases/latest"))
except Exception:
@@ -3455,7 +3545,7 @@ Categories=AudioVideo;Audio;Utility;
# Create a custom message box with checkbox
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Question)
msg_box.setIcon(QMessageBox.Icon.Question)
msg_box.setWindowTitle("Clear Cache Files")
msg_text = f"Found {file_count} cache file{'s' if file_count != 1 else ''} in the {PROGRAM_NAME} cache folder."
@@ -3474,10 +3564,12 @@ Categories=AudioVideo;Audio;Utility;
msg_box.setCheckBox(preview_cache_checkbox)
# Add buttons
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg_box.setDefaultButton(QMessageBox.Yes)
msg_box.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
msg_box.setDefaultButton(QMessageBox.StandardButton.Yes)
if msg_box.exec_() != QMessageBox.Yes:
if msg_box.exec() != QMessageBox.StandardButton.Yes:
return
# Delete the text files
@@ -3522,7 +3614,7 @@ Categories=AudioVideo;Audio;Utility;
def set_max_log_lines(self):
"""Open a dialog to set the maximum lines in the log window."""
from PyQt5.QtWidgets import QInputDialog
from PyQt6.QtWidgets import QInputDialog
value, ok = QInputDialog.getInt(
self,
@@ -3545,7 +3637,7 @@ Categories=AudioVideo;Audio;Utility;
def set_max_subtitle_words(self):
"""Open a dialog to set the maximum words per subtitle"""
from PyQt5.QtWidgets import QInputDialog
from PyQt6.QtWidgets import QInputDialog
current_value = self.config.get("max_subtitle_words", 50)
@@ -3574,7 +3666,6 @@ Categories=AudioVideo;Audio;Utility;
def set_silence_between_chapters(self):
"""Open a dialog to set the silence duration between chapters"""
from PyQt5.QtWidgets import QInputDialog, QDialog
current_value = self.config.get("silence_duration", 2.0)
@@ -3583,14 +3674,14 @@ Categories=AudioVideo;Audio;Utility;
dlg.setLabelText(
"Enter the duration of silence\nbetween chapters (in seconds):"
)
dlg.setInputMode(QInputDialog.DoubleInput)
dlg.setInputMode(QInputDialog.InputMode.DoubleInput)
dlg.setDoubleDecimals(1)
dlg.setDoubleMinimum(0.0)
dlg.setDoubleMaximum(60.0)
dlg.setDoubleValue(current_value)
dlg.setDoubleStep(0.1) # <-- set step to 0.1
if dlg.exec_() == QDialog.Accepted:
if dlg.exec() == QDialog.DialogCode.Accepted:
value = dlg.doubleValue()
# Round to one decimal to avoid floating-point representation noise
value = round(value, 1)
@@ -3606,6 +3697,7 @@ Categories=AudioVideo;Audio;Utility;
"Setting Saved",
f"Silence duration between chapters set to {value:.1f} seconds.",
)
def set_separate_chapters_format(self, fmt):
"""Set the format for separate chapters audio files."""
self.separate_chapters_format = fmt
+20 -9
View File
@@ -1,4 +1,3 @@
from json import load
import os
import sys
import platform
@@ -7,33 +6,43 @@ import signal
# Qt platform plugin detection (fixes #59)
try:
from PyQt5.QtCore import QLibraryInfo
plugins = QLibraryInfo.location(QLibraryInfo.PluginsPath)
from PyQt6.QtCore import QLibraryInfo
# Get the path to the plugins directory
plugins = QLibraryInfo.path(QLibraryInfo.LibraryPath.PluginsPath)
# Normalize path to use the OS-native separators and absolute path
platform_dir = os.path.normpath(os.path.join(plugins, "platforms"))
# Ensure we work with an absolute path for clarity
platform_dir = os.path.abspath(platform_dir)
if os.path.isdir(platform_dir):
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = platform_dir
print("QT_QPA_PLATFORM_PLUGIN_PATH set to:", platform_dir)
else:
print("PyQt5 platform plugins not found at", platform_dir)
print("PyQt6 platform plugins not found at", platform_dir)
except ImportError:
print("PyQt5 not installed.")
print("PyQt6 not installed.")
# Set application ID for Windows taskbar icon
if platform.system() == "Windows":
try:
from abogen.constants import PROGRAM_NAME, VERSION
import ctypes
app_id = f"{PROGRAM_NAME}.{VERSION}"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
except Exception as e:
print("Warning: failed to set AppUserModelID:", e)
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
from PyQt6.QtWidgets import QApplication
from PyQt6.QtGui import QIcon
from PyQt6.QtCore import (
QLibraryInfo,
qInstallMessageHandler,
QtMsgType,
)
# Add the directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
@@ -82,10 +91,12 @@ if platform.system() == "Darwin" and platform.processor() == "arm":
# Custom message handler to filter out specific Qt warnings
def qt_message_handler(mode, context, message):
# In PyQt6, the mode is an enum, so we compare with the enum members
if "Wayland does not support QWindow::requestActivate()" in message:
return # Suppress this specific message
if "setGrabPopup called with a parent, QtWaylandClient" in message:
return
if mode == QtMsgType.QtWarningMsg:
print(f"Qt Warning: {message}")
elif mode == QtMsgType.QtCriticalMsg:
@@ -129,7 +140,7 @@ def main():
ex = abogen()
ex.show()
sys.exit(app.exec_())
sys.exit(app.exec())
if __name__ == "__main__":
+216 -93
View File
@@ -2,7 +2,7 @@
# button to remove an item from the queue
# button to clear the queue
from PyQt5.QtWidgets import (
from PyQt6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
@@ -14,18 +14,20 @@ from PyQt5.QtWidgets import (
QLabel,
QWidget,
QSizePolicy,
QAbstractItemView,
)
from PyQt5.QtCore import QFileInfo, Qt
from PyQt6.QtCore import QFileInfo, Qt
from abogen.constants import COLORS
from copy import deepcopy
from PyQt5.QtGui import QFontMetrics
from PyQt6.QtGui import QFontMetrics
class ElidedLabel(QLabel):
def __init__(self, text, parent=None):
super().__init__(text, parent)
def __init__(self, text):
super().__init__(text)
self._full_text = text
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
self.setTextFormat(Qt.TextFormat.PlainText)
def setText(self, text):
self._full_text = text
@@ -34,7 +36,9 @@ class ElidedLabel(QLabel):
def resizeEvent(self, event):
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self._full_text, Qt.ElideRight, self.width())
elided = metrics.elidedText(
self._full_text, Qt.TextElideMode.ElideRight, self.width()
)
super().setText(elided)
super().resizeEvent(event)
@@ -53,8 +57,12 @@ class QueueListItemWidget(QWidget):
name_label = ElidedLabel(os.path.basename(file_name))
char_label = QLabel(f"Chars: {char_count}")
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
char_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
char_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
char_label.setAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
char_label.setSizePolicy(
QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred
)
layout.addWidget(name_label, 1)
layout.addWidget(char_label, 0)
self.setLayout(layout)
@@ -67,12 +75,14 @@ class DroppableQueueListWidget(QListWidget):
self.setAcceptDrops(True)
# Overlay for drag hover
self.drag_overlay = QLabel("", self)
self.drag_overlay.setAlignment(Qt.AlignCenter)
self.drag_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.drag_overlay.setStyleSheet(
f"border:2px dashed {COLORS['BLUE_BORDER_HOVER']}; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG_HOVER']};"
)
self.drag_overlay.setVisible(False)
self.drag_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
self.drag_overlay.setAttribute(
Qt.WidgetAttribute.WA_TransparentForMouseEvents, True
)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
@@ -132,9 +142,11 @@ class QueueManager(QDialog):
layout.setSpacing(12) # set spacing between widgets in main layout
# list of queued items
self.listwidget = DroppableQueueListWidget(self)
self.listwidget.setSelectionMode(QListWidget.ExtendedSelection)
self.listwidget.setSelectionMode(
QAbstractItemView.SelectionMode.ExtendedSelection
)
self.listwidget.setAlternatingRowColors(True)
self.listwidget.setContextMenuPolicy(Qt.CustomContextMenu)
self.listwidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.listwidget.customContextMenuRequested.connect(self.show_context_menu)
# Add informative instructions at the top
instructions = QLabel(
@@ -145,7 +157,7 @@ class QueueManager(QDialog):
"Changing the main window configuration afterward <b>does not</b> affect files already in the queue. "
"You can view each file's configuration by hovering over them."
)
instructions.setAlignment(Qt.AlignLeft)
instructions.setAlignment(Qt.AlignmentFlag.AlignLeft)
instructions.setWordWrap(True)
instructions.setStyleSheet("margin-bottom: 8px;")
layout.addWidget(instructions)
@@ -154,12 +166,14 @@ class QueueManager(QDialog):
"Drag and drop your text files here or use the 'Add files' button.",
self.listwidget,
)
self.empty_overlay.setAlignment(Qt.AlignCenter)
self.empty_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.empty_overlay.setStyleSheet(
f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;"
)
self.empty_overlay.setWordWrap(True)
self.empty_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
self.empty_overlay.setAttribute(
Qt.WidgetAttribute.WA_TransparentForMouseEvents, True
)
self.empty_overlay.hide()
# add queue items to the list
self.process_queue()
@@ -192,7 +206,10 @@ class QueueManager(QDialog):
self.listwidget.currentItemChanged.connect(self.update_button_states)
self.listwidget.itemSelectionChanged.connect(self.update_button_states)
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel,
self,
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
@@ -211,7 +228,6 @@ class QueueManager(QDialog):
self.listwidget.clear()
if not self.queue:
self.empty_overlay.resize(self.listwidget.size())
self.empty_overlay.show()
self.update_button_states()
return
@@ -224,8 +240,16 @@ class QueueManager(QDialog):
processing_file_path = item.file_name
# Normalize paths for consistent display (fixes Windows path separator issues)
display_file_path = os.path.normpath(display_file_path) if display_file_path else display_file_path
processing_file_path = os.path.normpath(processing_file_path) if processing_file_path else processing_file_path
display_file_path = (
os.path.normpath(display_file_path)
if display_file_path
else display_file_path
)
processing_file_path = (
os.path.normpath(processing_file_path)
if processing_file_path
else processing_file_path
)
# Only show the file name, not the full path
display_name = display_file_path
@@ -240,13 +264,19 @@ class QueueManager(QDialog):
# For plain .txt inputs we don't need to show a separate processing file
show_processing = True
try:
if isinstance(display_file_path, str) and display_file_path.lower().endswith('.txt'):
if isinstance(
display_file_path, str
) and display_file_path.lower().endswith(".txt"):
show_processing = False
except Exception:
show_processing = True
tooltip = f"<b>Input File:</b> {display_file_path}<br>"
if show_processing and processing_file_path and processing_file_path != display_file_path:
if (
show_processing
and processing_file_path
and processing_file_path != display_file_path
):
tooltip += f"<b>Processing File:</b> {processing_file_path}<br>"
tooltip += (
f"<b>Language:</b> {getattr(item, 'lang_code', '')}<br>"
@@ -262,13 +292,24 @@ class QueueManager(QDialog):
f"<b>Characters:</b> {getattr(item, 'total_char_count', '')}<br>"
f"<b>Replace Single Newlines:</b> {getattr(item, 'replace_single_newlines', False)}"
)
# Add book handler options if present
save_chapters_separately = getattr(item, "save_chapters_separately", None)
merge_chapters_at_end = getattr(item, "merge_chapters_at_end", None)
if save_chapters_separately is not None:
tooltip += f"<br><b>Save chapters separately:</b> {'Yes' if save_chapters_separately else 'No'}"
# Only show merge option if saving chapters separately
if save_chapters_separately and merge_chapters_at_end is not None:
tooltip += f"<br><b>Merge chapters at the end:</b> {'Yes' if merge_chapters_at_end else 'No'}"
list_item.setToolTip(tooltip)
list_item.setIcon(icon)
# Store both paths for context menu
list_item.setData(Qt.UserRole, {
'display_path': display_file_path,
'processing_path': processing_file_path
})
list_item.setData(
Qt.ItemDataRole.UserRole,
{
"display_path": display_file_path,
"processing_path": processing_file_path,
},
)
# Use custom widget for display
char_count = getattr(item, "total_char_count", 0)
widget = QueueListItemWidget(display_file_path, char_count)
@@ -280,7 +321,7 @@ class QueueManager(QDialog):
items = self.listwidget.selectedItems()
if not items:
return
from PyQt5.QtWidgets import QMessageBox
from PyQt6.QtWidgets import QMessageBox
# Remove by index to ensure correct mapping
rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
@@ -290,10 +331,10 @@ class QueueManager(QDialog):
self,
"Confirm Remove",
f"Are you sure you want to remove {len(rows)} selected items from the queue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.Yes:
if reply != QMessageBox.StandardButton.Yes:
return
for row in rows:
if 0 <= row < len(self.queue):
@@ -302,17 +343,17 @@ class QueueManager(QDialog):
self.update_button_states()
def clear_queue(self):
from PyQt5.QtWidgets import QMessageBox
from PyQt6.QtWidgets import QMessageBox
if len(self.queue) > 1:
reply = QMessageBox.question(
self,
"Confirm Clear Queue",
f"Are you sure you want to clear {len(self.queue)} items from the queue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.Yes:
if reply != QMessageBox.StandardButton.Yes:
return
self.queue.clear()
self.listwidget.clear()
@@ -362,6 +403,13 @@ class QueueManager(QDialog):
attrs["replace_single_newlines"] = getattr(
parent, "replace_single_newlines", False
)
# book handler options
attrs["save_chapters_separately"] = getattr(
parent, "save_chapters_separately", None
)
attrs["merge_chapters_at_end"] = getattr(
parent, "merge_chapters_at_end", None
)
else:
# fallback: empty values
attrs = {
@@ -378,11 +426,13 @@ class QueueManager(QDialog):
"replace_single_newlines",
]
}
attrs["save_chapters_separately"] = None
attrs["merge_chapters_at_end"] = None
return attrs
def add_files_from_paths(self, file_paths):
from abogen.utils import calculate_text_length
from PyQt5.QtWidgets import QMessageBox
from PyQt6.QtWidgets import QMessageBox
import os
current_attrs = self.get_current_attributes()
@@ -394,7 +444,9 @@ class QueueManager(QDialog):
item = QueueItem()
item.file_name = file_path
item.save_base_path = file_path # For .txt files, processing and save paths are the same
item.save_base_path = (
file_path # For .txt files, processing and save paths are the same
)
for attr, value in current_attrs.items():
setattr(item, attr, value)
# Read file content and calculate total_char_count using calculate_text_length
@@ -430,6 +482,10 @@ class QueueManager(QDialog):
== getattr(item, "replace_single_newlines", False)
and getattr(queued_item, "save_base_path", None)
== getattr(item, "save_base_path", None)
and getattr(queued_item, "save_chapters_separately", None)
== getattr(item, "save_chapters_separately", None)
and getattr(queued_item, "merge_chapters_at_end", None)
== getattr(item, "merge_chapters_at_end", None)
):
is_duplicate = True
break
@@ -447,7 +503,7 @@ class QueueManager(QDialog):
self.update_button_states()
def add_more_files(self):
from PyQt5.QtWidgets import QFileDialog
from PyQt6.QtWidgets import QFileDialog
from abogen.utils import calculate_text_length # import the function
# Only allow .txt files
@@ -477,9 +533,9 @@ class QueueManager(QDialog):
self.clear_button.setEnabled(bool(self.queue))
def show_context_menu(self, pos):
from PyQt5.QtWidgets import QMenu, QAction
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import QUrl
from PyQt6.QtWidgets import QMenu
from PyQt6.QtGui import QAction, QDesktopServices
from PyQt6.QtCore import QUrl
import os
global_pos = self.listwidget.viewport().mapToGlobal(pos)
@@ -491,75 +547,129 @@ class QueueManager(QDialog):
remove_action.triggered.connect(self.remove_item)
menu.addAction(remove_action)
# Add Open file action
open_file_action = QAction("Open file", self)
def open_file():
from PyQt5.QtWidgets import QMessageBox
# Get paths for determining if it's a document input
item = selected_items[0]
paths = item.data(Qt.UserRole)
paths = item.data(Qt.ItemDataRole.UserRole)
if isinstance(paths, dict):
file_path = paths.get('display_path', paths.get('processing_path', ''))
display_path = paths.get("display_path", "")
processing_path = paths.get("processing_path", "")
else:
file_path = paths # Fallback for old format
display_path = paths
processing_path = paths
# Find the queue item
doc_exts = (".md", ".markdown", ".pdf", ".epub")
is_document_input = (
isinstance(display_path, str)
and display_path.lower().endswith(doc_exts)
) or (
isinstance(processing_path, str)
and processing_path.lower().endswith(doc_exts)
)
# Add Open file action(s)
def open_file_by_path(path_label: str):
from PyQt6.QtWidgets import QMessageBox
p = display_path if path_label == "display" else processing_path
if not p:
QMessageBox.warning(
self, "File Not Found", "Path is not available."
)
return
# Find the queue item and resolve the target path
target_path = None
for q in self.queue:
if (getattr(q, "save_base_path", None) == file_path or
q.file_name == file_path):
target_path = getattr(q, "save_base_path", None) or q.file_name
if (
getattr(q, "save_base_path", None) == display_path
or q.file_name == display_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else:
target_path = q.file_name
break
if (
getattr(q, "save_base_path", None) == processing_path
or q.file_name == processing_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else:
target_path = q.file_name
break
# Fallback to the raw path if resolution failed
if not target_path:
target_path = p
if not os.path.exists(target_path):
QMessageBox.warning(
self, "File Not Found", f"The file does not exist."
)
return
QDesktopServices.openUrl(QUrl.fromLocalFile(target_path))
break
open_file_action.triggered.connect(open_file)
if is_document_input:
# For documents, show two open options
open_processed_action = QAction("Open processed file", self)
open_processed_action.triggered.connect(
lambda: open_file_by_path("processing")
)
menu.addAction(open_processed_action)
open_input_action = QAction("Open input file", self)
open_input_action.triggered.connect(
lambda: open_file_by_path("display")
)
menu.addAction(open_input_action)
else:
# For plain text files, show single open option
open_file_action = QAction("Open file", self)
open_file_action.triggered.connect(lambda: open_file_by_path("display"))
menu.addAction(open_file_action)
# Add Go to folder action
# If the queued item represents a converted document (markdown, pdf, epub)
# show two actions: Go to processed file (the cached .txt) and Go to input file (original source)
item = selected_items[0]
paths = item.data(Qt.UserRole)
if isinstance(paths, dict):
display_path = paths.get('display_path', '')
processing_path = paths.get('processing_path', '')
else:
display_path = paths
processing_path = paths
doc_exts = ('.md', '.markdown', '.pdf', '.epub')
is_document_input = (
isinstance(display_path, str) and display_path.lower().endswith(doc_exts)
) or (
isinstance(processing_path, str) and processing_path.lower().endswith(doc_exts)
)
from PyQt5.QtWidgets import QMessageBox
from PyQt6.QtWidgets import QMessageBox
def open_folder_for(path_label: str):
# path_label should be either 'display' or 'processing'
p = display_path if path_label == 'display' else processing_path
p = display_path if path_label == "display" else processing_path
if not p:
QMessageBox.warning(self, "File Not Found", "Path is not available.")
QMessageBox.warning(
self, "File Not Found", "Path is not available."
)
return
# If the stored path is the display path (original) but the actual file may be
# stored on the queue object differently, try to resolve via the queue entry.
target_path = None
for q in self.queue:
if getattr(q, 'save_base_path', None) == display_path or q.file_name == display_path:
if path_label == 'display':
target_path = getattr(q, 'save_base_path', None) or q.file_name
if (
getattr(q, "save_base_path", None) == display_path
or q.file_name == display_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else:
target_path = q.file_name
break
if getattr(q, 'save_base_path', None) == processing_path or q.file_name == processing_path:
if path_label == 'display':
target_path = getattr(q, 'save_base_path', None) or q.file_name
if (
getattr(q, "save_base_path", None) == processing_path
or q.file_name == processing_path
):
if path_label == "display":
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
else:
target_path = q.file_name
break
@@ -568,7 +678,11 @@ class QueueManager(QDialog):
target_path = p
if not os.path.exists(target_path):
QMessageBox.warning(self, "File Not Found", f"The file does not exist: {target_path}")
QMessageBox.warning(
self,
"File Not Found",
f"The file does not exist: {target_path}",
)
return
folder = os.path.dirname(target_path)
if os.path.exists(folder):
@@ -576,11 +690,13 @@ class QueueManager(QDialog):
if is_document_input:
processed_action = QAction("Go to processed file", self)
processed_action.triggered.connect(lambda: open_folder_for('processing'))
processed_action.triggered.connect(
lambda: open_folder_for("processing")
)
menu.addAction(processed_action)
input_action = QAction("Go to input file", self)
input_action.triggered.connect(lambda: open_folder_for('display'))
input_action.triggered.connect(lambda: open_folder_for("display"))
menu.addAction(input_action)
else:
# Default behavior for non-document inputs: single "Go to folder" action
@@ -588,15 +704,22 @@ class QueueManager(QDialog):
def go_to_folder():
item = selected_items[0]
paths = item.data(Qt.UserRole)
paths = item.data(Qt.ItemDataRole.UserRole)
if isinstance(paths, dict):
file_path = paths.get('display_path', paths.get('processing_path', ''))
file_path = paths.get(
"display_path", paths.get("processing_path", "")
)
else:
file_path = paths # Fallback for old format
# Find the queue item
for q in self.queue:
if (getattr(q, "save_base_path", None) == file_path or q.file_name == file_path):
target_path = getattr(q, "save_base_path", None) or q.file_name
if (
getattr(q, "save_base_path", None) == file_path
or q.file_name == file_path
):
target_path = (
getattr(q, "save_base_path", None) or q.file_name
)
if not os.path.exists(target_path):
QMessageBox.warning(
self, "File Not Found", f"The file does not exist."
@@ -618,7 +741,7 @@ class QueueManager(QDialog):
clear_action = QAction("Clear Queue", self)
clear_action.triggered.connect(self.clear_queue)
menu.addAction(clear_action)
menu.exec_(global_pos)
menu.exec(global_pos)
def accept(self):
# Accept: keep changes
@@ -626,7 +749,7 @@ class QueueManager(QDialog):
def reject(self):
# Cancel: restore original queue
from PyQt5.QtWidgets import QMessageBox
from PyQt6.QtWidgets import QMessageBox
# Warn if user changed a lot (e.g., more than 1 items difference)
original_count = len(self._original_queue)
@@ -636,19 +759,19 @@ class QueueManager(QDialog):
self,
"Confirm Cancel",
f"Are you sure you want to cancel and discard all changes?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.Yes:
if reply != QMessageBox.StandardButton.Yes:
return
self.queue.clear()
self.queue.extend(deepcopy(self._original_queue))
super().reject()
def keyPressEvent(self, event):
from PyQt5.QtCore import Qt
from PyQt6.QtCore import Qt
if event.key() == Qt.Key_Delete:
if event.key() == Qt.Key.Key_Delete:
self.remove_item()
else:
super().keyPressEvent(event)
+2
View File
@@ -15,3 +15,5 @@ class QueuedItem:
total_char_count: int
replace_single_newlines: bool = False
save_base_path: str = None
save_chapters_separately: bool = None
merge_chapters_at_end: bool = None
+2
View File
@@ -14,6 +14,7 @@ warnings.filterwarnings("ignore")
def detect_encoding(file_path):
import chardet
import charset_normalizer
with open(file_path, "rb") as f:
raw_data = f.read()
detected_encoding = None
@@ -28,6 +29,7 @@ def detect_encoding(file_path):
encoding = detected_encoding if detected_encoding else "utf-8"
return encoding.lower()
def get_resource_path(package, resource):
"""
Get the path to a resource file, with fallback to local file system.
+156 -83
View File
@@ -1,6 +1,6 @@
import json
import os
from PyQt5.QtWidgets import (
from PyQt6.QtWidgets import (
QDialog,
QVBoxLayout,
QCheckBox,
@@ -22,12 +22,11 @@ from PyQt5.QtWidgets import (
QFileDialog,
QSplitter,
QMenu,
QAction,
QComboBox,
QApplication,
QComboBox,
)
from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize
from PyQt5.QtGui import QPixmap, QIcon
from PyQt6.QtCore import Qt, QTimer, QPoint, QRect, QSize
from PyQt6.QtGui import QPixmap, QIcon, QAction
from abogen.constants import (
VOICES_INTERNAL,
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
@@ -91,7 +90,7 @@ class FlowLayout(QLayout):
return len(self._item_list)
def expandingDirections(self):
return Qt.Orientations(Qt.Orientation(0))
return Qt.Orientation(0)
def hasHeightForWidth(self):
return True
@@ -132,10 +131,14 @@ class FlowLayout(QLayout):
for item in self._item_list:
style = self.parentWidget().style() if self.parentWidget() else QStyle()
layout_spacing_x = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
QSizePolicy.ControlType.PushButton,
QSizePolicy.ControlType.PushButton,
Qt.Orientation.Horizontal,
)
layout_spacing_y = style.layoutSpacing(
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
QSizePolicy.ControlType.PushButton,
QSizePolicy.ControlType.PushButton,
Qt.Orientation.Vertical,
)
space_x = spacing if spacing >= 0 else layout_spacing_x
space_y = spacing if spacing >= 0 else layout_spacing_y
@@ -163,7 +166,7 @@ class VoiceMixer(QWidget):
super().__init__()
self.voice_name = voice_name
self.setFixedWidth(VOICE_MIXER_WIDTH)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# TODO Set CSS for rounded corners
# self.setObjectName("VoiceMixer")
@@ -173,7 +176,7 @@ class VoiceMixer(QWidget):
# Name label at the top
name = voice_name
layout.addWidget(QLabel(name), alignment=Qt.AlignCenter)
layout.addWidget(QLabel(name), alignment=Qt.AlignmentFlag.AlignCenter)
# Voice name label with gender icon
is_female = self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f"
@@ -181,7 +184,9 @@ class VoiceMixer(QWidget):
# Icons layout (flag and gender)
icons_layout = QHBoxLayout()
icons_layout.setSpacing(3)
icons_layout.setAlignment(Qt.AlignCenter) # Center the icons horizontally
icons_layout.setAlignment(
Qt.AlignmentFlag.AlignCenter
) # Center the icons horizontally
# Flag icon
flag_icon_path = get_resource_path(
@@ -194,11 +199,21 @@ class VoiceMixer(QWidget):
gender_label = QLabel()
flag_pixmap = QPixmap(flag_icon_path)
flag_label.setPixmap(
flag_pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)
flag_pixmap.scaled(
16,
16,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
)
gender_pixmap = QPixmap(gender_icon_path)
gender_label.setPixmap(
gender_pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)
gender_pixmap.scaled(
16,
16,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
)
icons_layout.addWidget(flag_label)
icons_layout.addWidget(gender_label)
@@ -210,7 +225,7 @@ class VoiceMixer(QWidget):
self.checkbox = QCheckBox()
self.checkbox.setChecked(initial_status)
self.checkbox.stateChanged.connect(self.toggle_inputs)
layout.addWidget(self.checkbox, alignment=Qt.AlignCenter)
layout.addWidget(self.checkbox, alignment=Qt.AlignmentFlag.AlignCenter)
# Spinbox and slider
self.spin_box = QDoubleSpinBox()
@@ -219,12 +234,51 @@ class VoiceMixer(QWidget):
self.spin_box.setDecimals(2)
self.spin_box.setValue(initial_weight)
self.slider = QSlider(Qt.Vertical)
self.slider = QSlider(Qt.Orientation.Vertical)
self.slider.setRange(0, 100)
self.slider.setValue(int(initial_weight * 100))
self.slider.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.slider.setSizePolicy(
QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding
)
self.slider.setFixedWidth(SLIDER_WIDTH)
# Apply slider styling after widget is added to window (see showEvent)
self._slider_style_applied = False
# Connect controls
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
self.spin_box.valueChanged.connect(
lambda val: self.slider.setValue(int(val * 100))
)
# Layout for slider and labels
slider_layout = QVBoxLayout()
slider_layout.addWidget(self.spin_box)
slider_layout.addWidget(QLabel("1", alignment=Qt.AlignmentFlag.AlignCenter))
slider_center_layout = QHBoxLayout()
slider_center_layout.addWidget(
self.slider, alignment=Qt.AlignmentFlag.AlignHCenter
)
slider_center_layout.setContentsMargins(0, 0, 0, 0)
slider_center_widget = QWidget()
slider_center_widget.setLayout(slider_center_layout)
slider_layout.addWidget(slider_center_widget, stretch=1)
slider_layout.addWidget(QLabel("0", alignment=Qt.AlignmentFlag.AlignCenter))
slider_layout.setStretch(2, 1)
layout.addLayout(slider_layout, stretch=1)
self.setLayout(layout)
self.toggle_inputs()
def showEvent(self, event):
super().showEvent(event)
# Apply slider styling once when widget is shown and has access to parent
if not self._slider_style_applied:
self._slider_style_applied = True
# Fix slider in Windows
if platform.system() == "Windows":
appstyle = QApplication.instance().style().objectName().lower()
@@ -239,33 +293,28 @@ class VoiceMixer(QWidget):
}}
"""
)
else:
# Apply same fix for Light theme on non-Windows systems
# Get theme from parent window's config
parent_window = self.window()
theme = "system"
while parent_window:
if hasattr(parent_window, "config"):
theme = parent_window.config.get("theme", "system")
break
parent_window = parent_window.parent()
# Connect controls
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
self.spin_box.valueChanged.connect(
lambda val: self.slider.setValue(int(val * 100))
if theme == "light":
self.slider.setStyleSheet(
f"""
QSlider::groove:vertical:disabled {{
background: {COLORS.get("GREY_BACKGROUND")};
width: 4px;
border-radius: 4px;
}}
"""
)
# Layout for slider and labels
slider_layout = QVBoxLayout()
slider_layout.addWidget(self.spin_box)
slider_layout.addWidget(QLabel("1", alignment=Qt.AlignCenter))
slider_center_layout = QHBoxLayout()
slider_center_layout.addWidget(self.slider, alignment=Qt.AlignHCenter)
slider_center_layout.setContentsMargins(0, 0, 0, 0)
slider_center_widget = QWidget()
slider_center_widget.setLayout(slider_center_layout)
slider_layout.addWidget(slider_center_widget, stretch=1)
slider_layout.addWidget(QLabel("0", alignment=Qt.AlignCenter))
slider_layout.setStretch(2, 1)
layout.addLayout(slider_layout, stretch=1)
self.setLayout(layout)
self.toggle_inputs()
def toggle_inputs(self):
is_enabled = self.checkbox.isChecked()
self.spin_box.setEnabled(is_enabled)
@@ -307,9 +356,11 @@ class HoverLabel(QLabel):
"""
)
# Make sure the entire button is clickable, not just the text
self.delete_button.setFocusPolicy(Qt.NoFocus)
self.delete_button.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.delete_button.setCursor(Qt.PointingHandCursor)
self.delete_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.delete_button.setAttribute(
Qt.WidgetAttribute.WA_TransparentForMouseEvents, False
)
self.delete_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.delete_button.hide()
def resizeEvent(self, event):
@@ -357,7 +408,7 @@ class VoiceFormulaDialog(QDialog):
if parent is not None and hasattr(parent, "subtitle_combo"):
self.subtitle_combo = parent.subtitle_combo
# Create main container layout with profile section and mixer section
splitter = QSplitter(Qt.Horizontal)
splitter = QSplitter(Qt.Orientation.Horizontal)
# Profile section
profile_widget = QWidget()
profile_layout = QVBoxLayout(profile_widget)
@@ -371,8 +422,8 @@ class VoiceFormulaDialog(QDialog):
profile_layout.addLayout(header_layout)
# Profile list
self.profile_list = QListWidget()
self.profile_list.setSelectionMode(QListWidget.SingleSelection)
self.profile_list.setSelectionBehavior(QListWidget.SelectRows)
self.profile_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
self.profile_list.setSelectionBehavior(QListWidget.SelectionBehavior.SelectRows)
self.profile_list.setStyleSheet(
"QListWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }"
)
@@ -388,7 +439,7 @@ class VoiceFormulaDialog(QDialog):
idx = list(profiles.keys()).index(self.current_profile)
self.profile_list.setCurrentRow(idx)
profile_layout.addWidget(self.profile_list)
self.profile_list.setContextMenuPolicy(Qt.CustomContextMenu)
self.profile_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.profile_list.customContextMenuRequested.connect(
self.show_profile_context_menu
)
@@ -409,7 +460,9 @@ class VoiceFormulaDialog(QDialog):
self.setWindowTitle("Voice Mixer")
self.setWindowFlags(
Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint
Qt.WindowType.Window
| Qt.WindowType.WindowCloseButtonHint
| Qt.WindowType.WindowMaximizeButtonHint
)
self.setMinimumSize(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)
@@ -468,21 +521,25 @@ class VoiceFormulaDialog(QDialog):
# Separator
separator = QFrame()
separator.setFrameShadow(QFrame.Sunken)
separator.setFrameShadow(QFrame.Shadow.Sunken)
mixer_layout.addWidget(separator)
# Voice list scroll area
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.scroll_area.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
self.scroll_area.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
self.scroll_area.viewport().installEventFilter(self)
self.voice_list_widget = QWidget()
self.voice_list_layout = QHBoxLayout()
self.voice_list_widget.setLayout(self.voice_list_layout)
self.voice_list_widget.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Expanding
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
)
self.scroll_area.setWidget(self.voice_list_widget)
mixer_layout.addWidget(self.scroll_area, stretch=1)
@@ -497,6 +554,9 @@ class VoiceFormulaDialog(QDialog):
ok_button.setDefault(True)
ok_button.setFocus()
# Connect buttons
clear_all_button.clicked.connect(self.clear_all_voices)
ok_button.clicked.connect(self.accept)
# Connect buttons
clear_all_button.clicked.connect(self.clear_all_voices)
ok_button.clicked.connect(self.accept)
@@ -533,7 +593,7 @@ class VoiceFormulaDialog(QDialog):
def keyPressEvent(self, event):
# Bind Delete key to delete_profile when a profile is selected
if event.key() == Qt.Key_Delete and self.profile_list.hasFocus():
if event.key() == Qt.Key.Key_Delete and self.profile_list.hasFocus():
item = self.profile_list.currentItem()
if item:
self.delete_profile(item)
@@ -562,10 +622,12 @@ class VoiceFormulaDialog(QDialog):
self,
"Unsaved Changes",
msg,
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
QMessageBox.Save,
QMessageBox.StandardButton.Save
| QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Save,
)
if ret == QMessageBox.Save:
if ret == QMessageBox.StandardButton.Save:
# Save all using stored states
profiles = load_profiles()
for i in dirty_indices:
@@ -590,7 +652,7 @@ class VoiceFormulaDialog(QDialog):
self.update_profile_save_buttons()
self.update_profile_list_colors()
return True
elif ret == QMessageBox.Discard:
elif ret == QMessageBox.StandardButton.Discard:
# Discard all modifications
self._profile_states.clear()
for i in dirty_indices:
@@ -612,17 +674,19 @@ class VoiceFormulaDialog(QDialog):
else:
# Fallback to original logic for 0 or 1 dirty profile
box = QMessageBox(self)
box.setIcon(QMessageBox.Warning)
box.setIcon(QMessageBox.Icon.Warning)
box.setWindowTitle("Unsaved Changes")
box.setText(
"You have unsaved changes in your profile. Do you want to save the changes?"
)
box.setStandardButtons(
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
QMessageBox.StandardButton.Save
| QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel
)
box.setDefaultButton(QMessageBox.Save)
ret = box.exec_()
if ret == QMessageBox.Save:
box.setDefaultButton(QMessageBox.StandardButton.Save)
ret = box.exec()
if ret == QMessageBox.StandardButton.Save:
for i in range(self.profile_list.count()):
item = self.profile_list.item(i)
name = item.text().lstrip("*")
@@ -636,7 +700,7 @@ class VoiceFormulaDialog(QDialog):
if hasattr(parent, "populate_profiles_in_voice_combo"):
parent.populate_profiles_in_voice_combo()
return True
elif ret == QMessageBox.Discard:
elif ret == QMessageBox.StandardButton.Discard:
profiles = load_profiles()
for i in range(self.profile_list.count()):
item = self.profile_list.item(i)
@@ -717,7 +781,7 @@ class VoiceFormulaDialog(QDialog):
return voice_mixer
def handle_voice_checkbox(self, voice_mixer, state):
if state == Qt.Checked:
if state == Qt.CheckState.Checked.value:
self.last_enabled_voice = voice_mixer.voice_name
self.update_weighted_sums()
@@ -767,7 +831,9 @@ class VoiceFormulaDialog(QDialog):
f'<b><span style="color:{COLORS.get("BLUE")}">{name}: {percentage:.1f}%</span></b>',
name,
)
voice_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
voice_label.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred
)
voice_label.delete_button.clicked.connect(
lambda _, vn=name: self.disable_voice_by_name(vn)
)
@@ -787,7 +853,7 @@ class VoiceFormulaDialog(QDialog):
mixer.checkbox.setChecked(False)
def eventFilter(self, source, event):
if source is self.scroll_area.viewport() and event.type() == event.Wheel:
if source is self.scroll_area.viewport() and event.type() == event.Type.Wheel:
# Skip if over an enabled slider
if any(
mixer.slider.underMouse() and mixer.slider.isEnabled()
@@ -890,10 +956,10 @@ class VoiceFormulaDialog(QDialog):
self,
"Invalid Profiles",
msg,
QMessageBox.Yes | QMessageBox.Cancel,
QMessageBox.Yes,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Yes,
)
if reply == QMessageBox.Yes:
if reply == QMessageBox.StandardButton.Yes:
for i, name in reversed(zero):
self.profile_list.takeItem(i)
delete_profile(name)
@@ -980,8 +1046,8 @@ class VoiceFormulaDialog(QDialog):
super().closeEvent(event)
def _parse_rgba_to_qcolor(self, rgba_str):
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor
"""Helper to convert 'rgba(R,G,B,A_float)' string to QColor."""
match = re.match(r"rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)", rgba_str)
@@ -1136,9 +1202,12 @@ class VoiceFormulaDialog(QDialog):
msg += f"\nThis will overwrite an existing profile."
msg += "\nContinue?"
reply = QMessageBox.question(
self, "Import Profile", msg, QMessageBox.Yes | QMessageBox.No
self,
"Import Profile",
msg,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.Yes:
if reply != QMessageBox.StandardButton.Yes:
return
profiles.update(imported_profiles)
save_profiles(profiles)
@@ -1153,9 +1222,12 @@ class VoiceFormulaDialog(QDialog):
msg += f"\n{len(collisions)} profile(s) will be overwritten."
msg += "\nContinue?"
reply = QMessageBox.question(
self, "Import Profiles", msg, QMessageBox.Yes | QMessageBox.No
self,
"Import Profiles",
msg,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.Yes:
if reply != QMessageBox.StandardButton.Yes:
return
profiles.update(imported_profiles)
save_profiles(profiles)
@@ -1196,7 +1268,7 @@ class VoiceFormulaDialog(QDialog):
menu.addAction(dup_act)
menu.addAction(export_act)
menu.addAction(delete_act)
act = menu.exec_(self.profile_list.viewport().mapToGlobal(pos))
act = menu.exec(self.profile_list.viewport().mapToGlobal(pos))
if act == rename_act:
self.rename_profile(item)
elif act == delete_act:
@@ -1323,9 +1395,9 @@ class VoiceFormulaDialog(QDialog):
self,
"Delete Profile",
f"Delete profile '{name}'?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.Yes:
if reply == QMessageBox.StandardButton.Yes:
delete_profile(name)
row = self.profile_list.row(item)
self.profile_list.takeItem(row)
@@ -1378,7 +1450,7 @@ class VoiceFormulaDialog(QDialog):
self.profile_list.setItemWidget(item, widget)
def update_profile_list_colors(self):
from PyQt5.QtCore import Qt
from PyQt6.QtCore import Qt
profiles = load_profiles()
for i in range(self.profile_list.count()):
@@ -1386,13 +1458,14 @@ class VoiceFormulaDialog(QDialog):
name = item.text().lstrip("*")
if self._virtual_new_profile and name == "New profile":
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
item.setData(Qt.BackgroundRole, color)
item.setData(Qt.ItemDataRole.BackgroundRole, color)
elif item.text().startswith("*"):
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
item.setData(Qt.BackgroundRole, color)
item.setData(Qt.ItemDataRole.BackgroundRole, color)
else:
item.setData(
Qt.BackgroundRole, self.profile_list.palette().base().color()
Qt.ItemDataRole.BackgroundRole,
self.profile_list.palette().base().color(),
)
weights = profiles.get(name, {}).get("voices", [])
total = 0
@@ -1406,7 +1479,7 @@ class VoiceFormulaDialog(QDialog):
total += entry[1]
if total == 0:
color = self._parse_rgba_to_qcolor(COLORS.get("RED_BACKGROUND"))
item.setData(Qt.BackgroundRole, color)
item.setData(Qt.ItemDataRole.BackgroundRole, color)
self.update_profile_save_buttons()
def preview_current_mix(self):
+1 -1
View File
@@ -13,7 +13,7 @@ license = "MIT"
requires-python = ">=3.10, <3.13"
keywords = ["audiobook", "epub", "pdf", "text-to-speech", "subtitle", "tts", "kokoro", "accessibility", "book-converter", "voice-synthesis", "multilingual", "chapter-management", "subtitles", "content-creation", "media-generation"]
dependencies = [
"PyQt5>=5.15.11",
"PyQt6>=6.10.0",
"kokoro>=0.9.4",
"misaki[zh]>=0.9.4",
"ebooklib>=0.19",