mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c994c2eeb0 | ||
|
|
e4fe8122ff | ||
|
|
4cef65e70e | ||
|
|
24d5588f09 | ||
|
|
7883b8865b | ||
|
|
47fe6341b4 | ||
|
|
9d2e8bed00 | ||
|
|
eedf347866 | ||
|
|
0d0d3b7871 | ||
|
|
f80c86b00d |
@@ -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
|
# 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 `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.
|
- Added a loading indicator to the book handler window for better user experience during book preprocessing.
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ RUN apt-get update \
|
|||||||
python3 \
|
python3 \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-pyqt5 \
|
python3-pyqt6 \
|
||||||
espeak-ng \
|
espeak-ng \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.2.0
|
1.2.1
|
||||||
+389
-247
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,7 @@ COLORS = {
|
|||||||
"BLUE_BORDER_HOVER": "#6ab0de",
|
"BLUE_BORDER_HOVER": "#6ab0de",
|
||||||
"YELLOW_BACKGROUND": "rgba(255, 221, 51, 0.40)",
|
"YELLOW_BACKGROUND": "rgba(255, 221, 51, 0.40)",
|
||||||
"GREY_BACKGROUND": "rgba(128, 128, 128, 0.15)",
|
"GREY_BACKGROUND": "rgba(128, 128, 128, 0.15)",
|
||||||
|
"GREY_BORDER": "#808080",
|
||||||
"RED_BACKGROUND": "rgba(232, 78, 60, 0.15)",
|
"RED_BACKGROUND": "rgba(232, 78, 60, 0.15)",
|
||||||
"RED_BG": "rgba(232, 78, 60, 0.10)",
|
"RED_BG": "rgba(232, 78, 60, 0.10)",
|
||||||
"RED_BG_HOVER": "rgba(232, 78, 60, 0.15)",
|
"RED_BG_HOVER": "rgba(232, 78, 60, 0.15)",
|
||||||
|
|||||||
+113
-48
@@ -3,10 +3,15 @@ import re
|
|||||||
import time
|
import time
|
||||||
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, QTimer
|
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer
|
||||||
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
from PyQt6.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
||||||
import soundfile as sf
|
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 (
|
from abogen.constants import (
|
||||||
LANGUAGE_DESCRIPTIONS,
|
LANGUAGE_DESCRIPTIONS,
|
||||||
SAMPLE_VOICE_TEXTS,
|
SAMPLE_VOICE_TEXTS,
|
||||||
@@ -47,43 +52,45 @@ def sanitize_name_for_os(name, is_folder=True):
|
|||||||
if system == "Windows":
|
if system == "Windows":
|
||||||
# Windows illegal characters: < > : " / \ | ? *
|
# Windows illegal characters: < > : " / \ | ? *
|
||||||
# Also can't end with space or dot
|
# Also can't end with space or dot
|
||||||
sanitized = re.sub(r'[<>:"/\\|?*]', '_', name)
|
sanitized = re.sub(r'[<>:"/\\|?*]', "_", name)
|
||||||
# Remove control characters (0-31)
|
# Remove control characters (0-31)
|
||||||
sanitized = re.sub(r'[\x00-\x1f]', '_', sanitized)
|
sanitized = re.sub(r"[\x00-\x1f]", "_", sanitized)
|
||||||
# Remove trailing spaces and dots
|
# Remove trailing spaces and dots
|
||||||
sanitized = sanitized.rstrip('. ')
|
sanitized = sanitized.rstrip(". ")
|
||||||
# Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
# Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
||||||
reserved = ['CON', 'PRN', 'AUX', 'NUL'] + \
|
reserved = (
|
||||||
[f'COM{i}' for i in range(1, 10)] + \
|
["CON", "PRN", "AUX", "NUL"]
|
||||||
[f'LPT{i}' for i in range(1, 10)]
|
+ [f"COM{i}" for i in range(1, 10)]
|
||||||
if sanitized.upper() in reserved or sanitized.upper().split('.')[0] in reserved:
|
+ [f"LPT{i}" for i in range(1, 10)]
|
||||||
|
)
|
||||||
|
if sanitized.upper() in reserved or sanitized.upper().split(".")[0] in reserved:
|
||||||
sanitized = f"_{sanitized}"
|
sanitized = f"_{sanitized}"
|
||||||
elif system == "Darwin": # macOS
|
elif system == "Darwin": # macOS
|
||||||
# macOS illegal characters: : (colon is converted to / by the system)
|
# macOS illegal characters: : (colon is converted to / by the system)
|
||||||
# Also can't start with dot (hidden file) for folders typically
|
# Also can't start with dot (hidden file) for folders typically
|
||||||
sanitized = re.sub(r'[:]', '_', name)
|
sanitized = re.sub(r"[:]", "_", name)
|
||||||
# Remove control characters
|
# 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)
|
# Avoid leading dot for folders (creates hidden folders)
|
||||||
if is_folder and sanitized.startswith('.'):
|
if is_folder and sanitized.startswith("."):
|
||||||
sanitized = '_' + sanitized[1:]
|
sanitized = "_" + sanitized[1:]
|
||||||
else: # Linux and others
|
else: # Linux and others
|
||||||
# Linux illegal characters: / and null character
|
# Linux illegal characters: / and null character
|
||||||
# Though / is illegal, most other chars are technically allowed
|
# 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
|
# 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)
|
# Avoid leading dot for folders (creates hidden folders)
|
||||||
if is_folder and sanitized.startswith('.'):
|
if is_folder and sanitized.startswith("."):
|
||||||
sanitized = '_' + sanitized[1:]
|
sanitized = "_" + sanitized[1:]
|
||||||
|
|
||||||
# Ensure the name is not empty after sanitization
|
# Ensure the name is not empty after sanitization
|
||||||
if not sanitized or sanitized.strip() == '':
|
if not sanitized or sanitized.strip() == "":
|
||||||
sanitized = "audiobook"
|
sanitized = "audiobook"
|
||||||
|
|
||||||
# Limit length to 255 characters (common limit across filesystems)
|
# Limit length to 255 characters (common limit across filesystems)
|
||||||
if len(sanitized) > 255:
|
if len(sanitized) > 255:
|
||||||
sanitized = sanitized[:255].rstrip('. ')
|
sanitized = sanitized[:255].rstrip(". ")
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
|
|
||||||
@@ -96,8 +103,8 @@ class ChapterOptionsDialog(QDialog):
|
|||||||
# Prevent closing with the X button and remove the help button
|
# Prevent closing with the X button and remove the help button
|
||||||
self.setWindowFlags(
|
self.setWindowFlags(
|
||||||
self.windowFlags()
|
self.windowFlags()
|
||||||
& ~Qt.WindowCloseButtonHint
|
& ~Qt.WindowType.WindowCloseButtonHint
|
||||||
& ~Qt.WindowContextHelpButtonHint
|
& ~Qt.WindowType.WindowContextHelpButtonHint
|
||||||
)
|
)
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
@@ -131,7 +138,7 @@ class ChapterOptionsDialog(QDialog):
|
|||||||
layout.addWidget(self.countdown_label)
|
layout.addWidget(self.countdown_label)
|
||||||
|
|
||||||
# Add OK button
|
# Add OK button
|
||||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok)
|
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
||||||
button_box.accepted.connect(self.accept)
|
button_box.accepted.connect(self.accept)
|
||||||
layout.addWidget(button_box)
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
@@ -180,7 +187,7 @@ class ChapterOptionsDialog(QDialog):
|
|||||||
|
|
||||||
# Prevent escape key from closing the dialog
|
# Prevent escape key from closing the dialog
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
if event.key() == Qt.Key_Escape:
|
if event.key() == Qt.Key.Key_Escape:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
else:
|
else:
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
@@ -310,7 +317,11 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
# Normalize paths for consistent display (fixes Windows path separator issues)
|
# Normalize paths for consistent display (fixes Windows path separator issues)
|
||||||
input_file = os.path.normpath(input_file) if input_file else input_file
|
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}")
|
self.log_updated.emit(f"- Input File: {input_file}")
|
||||||
if input_file != processing_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
|
# Use file_name for logs if from_queue, otherwise use display_path if available
|
||||||
if getattr(self, "from_queue", False):
|
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:
|
else:
|
||||||
base_path = self.display_path if self.display_path else self.file_name
|
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
|
# Use file_name for logs if from_queue, otherwise use display_path if available
|
||||||
if getattr(self, "from_queue", False):
|
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:
|
else:
|
||||||
base_path = self.display_path if self.display_path else self.file_name
|
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
|
# Prepare merged output file for incremental writing ONLY if merge_chapters_at_end is True
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
out_dir = parent_dir
|
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}"
|
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
||||||
subtitle_entries = []
|
subtitle_entries = []
|
||||||
current_time = 0.0
|
current_time = 0.0
|
||||||
@@ -642,8 +659,12 @@ class ConversionThread(QThread):
|
|||||||
# Add style definitions for karaoke highlighting
|
# Add style definitions for karaoke highlighting
|
||||||
if self.subtitle_mode == "Sentence + Highlighting":
|
if self.subtitle_mode == "Sentence + Highlighting":
|
||||||
merged_subtitle_file.write("[V4+ Styles]\n")
|
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(
|
||||||
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")
|
"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("[Events]\n")
|
||||||
merged_subtitle_file.write(
|
merged_subtitle_file.write(
|
||||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
"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
|
# Add style definitions for karaoke highlighting
|
||||||
if self.subtitle_mode == "Sentence + Highlighting":
|
if self.subtitle_mode == "Sentence + Highlighting":
|
||||||
chapter_subtitle_file.write("[V4+ Styles]\n")
|
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(
|
||||||
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")
|
"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("[Events]\n")
|
||||||
chapter_subtitle_file.write(
|
chapter_subtitle_file.write(
|
||||||
@@ -922,7 +947,12 @@ class ConversionThread(QThread):
|
|||||||
start_time = self._ass_time(start)
|
start_time = self._ass_time(start)
|
||||||
end_time = self._ass_time(end)
|
end_time = self._ass_time(end)
|
||||||
# Use karaoke effect for highlighting mode
|
# 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(
|
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"
|
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)
|
start_time = self._ass_time(start)
|
||||||
end_time = self._ass_time(end)
|
end_time = self._ass_time(end)
|
||||||
# Use karaoke effect for highlighting mode
|
# 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(
|
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"
|
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)
|
# Add silence between chapters for merged output (except after the last chapter)
|
||||||
if merge_chapters_at_end and chapter_idx < total_chapters:
|
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_audio = self.np.zeros(silence_samples, dtype="float32")
|
||||||
silence_bytes = silence_audio.tobytes()
|
silence_bytes = silence_audio.tobytes()
|
||||||
|
|
||||||
@@ -1297,7 +1334,11 @@ class ConversionThread(QThread):
|
|||||||
karaoke_text = ""
|
karaoke_text = ""
|
||||||
for t in current_sentence:
|
for t in current_sentence:
|
||||||
# Calculate duration in centiseconds
|
# 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)
|
duration_cs = int(duration * 100)
|
||||||
# Add karaoke effect - relies on style's SecondaryColour for highlighting
|
# Add karaoke effect - relies on style's SecondaryColour for highlighting
|
||||||
karaoke_text += f"{{\\kf{duration_cs}}}{t['text']}{t.get('whitespace', '') or ''}"
|
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)
|
subtitle_entries[-1] = (start, fallback_end_time, text)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Word count-based grouping
|
# Word count-based grouping - simply count spaces and split after N spaces
|
||||||
try:
|
try:
|
||||||
word_count = int(self.subtitle_mode.split()[0])
|
word_count = int(self.subtitle_mode.split()[0])
|
||||||
word_count = min(word_count, max_subtitle_words)
|
word_count = min(word_count, max_subtitle_words)
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
word_count = 1
|
word_count = 1
|
||||||
|
|
||||||
# Group words into subtitle entries (processed_tokens already has punctuation combined)
|
current_group = []
|
||||||
for i in range(0, len(processed_tokens), word_count):
|
space_count = 0
|
||||||
group = processed_tokens[i : i + word_count]
|
|
||||||
if group:
|
for token in processed_tokens:
|
||||||
text = "".join(
|
current_group.append(token)
|
||||||
t["text"] + (t.get("whitespace", "") or "") for t in group
|
|
||||||
)
|
# Count spaces after tokens (in the whitespace field)
|
||||||
subtitle_entries.append(
|
if token.get("whitespace", "") == " ":
|
||||||
(group[0]["start"], group[-1]["end"], text.strip())
|
space_count += 1
|
||||||
)
|
|
||||||
|
# Split after counting N spaces
|
||||||
|
if space_count >= word_count:
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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
|
# Fallback for last entry
|
||||||
if subtitle_entries and fallback_end_time is not None:
|
if subtitle_entries and fallback_end_time is not None:
|
||||||
last_entry = subtitle_entries[-1]
|
last_entry = subtitle_entries[-1]
|
||||||
@@ -1406,7 +1472,6 @@ class ConversionThread(QThread):
|
|||||||
if end is None or end <= start or end <= 0:
|
if end is None or end <= start or end <= 0:
|
||||||
subtitle_entries[-1] = (start, fallback_end_time, text)
|
subtitle_entries[-1] = (start, fallback_end_time, text)
|
||||||
|
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
self.cancel_requested = True
|
self.cancel_requested = True
|
||||||
self.should_cancel = True
|
self.should_cancel = True
|
||||||
|
|||||||
+280
-188
File diff suppressed because it is too large
Load Diff
+20
-9
@@ -1,4 +1,3 @@
|
|||||||
from json import load
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
@@ -7,33 +6,43 @@ import signal
|
|||||||
|
|
||||||
# Qt platform plugin detection (fixes #59)
|
# Qt platform plugin detection (fixes #59)
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtCore import QLibraryInfo
|
from PyQt6.QtCore import QLibraryInfo
|
||||||
plugins = QLibraryInfo.location(QLibraryInfo.PluginsPath)
|
|
||||||
|
# Get the path to the plugins directory
|
||||||
|
plugins = QLibraryInfo.path(QLibraryInfo.LibraryPath.PluginsPath)
|
||||||
|
|
||||||
# Normalize path to use the OS-native separators and absolute path
|
# Normalize path to use the OS-native separators and absolute path
|
||||||
platform_dir = os.path.normpath(os.path.join(plugins, "platforms"))
|
platform_dir = os.path.normpath(os.path.join(plugins, "platforms"))
|
||||||
|
|
||||||
# Ensure we work with an absolute path for clarity
|
# Ensure we work with an absolute path for clarity
|
||||||
platform_dir = os.path.abspath(platform_dir)
|
platform_dir = os.path.abspath(platform_dir)
|
||||||
|
|
||||||
if os.path.isdir(platform_dir):
|
if os.path.isdir(platform_dir):
|
||||||
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = platform_dir
|
os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = platform_dir
|
||||||
print("QT_QPA_PLATFORM_PLUGIN_PATH set to:", platform_dir)
|
print("QT_QPA_PLATFORM_PLUGIN_PATH set to:", platform_dir)
|
||||||
else:
|
else:
|
||||||
print("PyQt5 platform plugins not found at", platform_dir)
|
print("PyQt6 platform plugins not found at", platform_dir)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("PyQt5 not installed.")
|
print("PyQt6 not installed.")
|
||||||
|
|
||||||
# Set application ID for Windows taskbar icon
|
# Set application ID for Windows taskbar icon
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
try:
|
try:
|
||||||
from abogen.constants import PROGRAM_NAME, VERSION
|
from abogen.constants import PROGRAM_NAME, VERSION
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
app_id = f"{PROGRAM_NAME}.{VERSION}"
|
app_id = f"{PROGRAM_NAME}.{VERSION}"
|
||||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Warning: failed to set AppUserModelID:", e)
|
print("Warning: failed to set AppUserModelID:", e)
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
|
from PyQt6.QtCore import (
|
||||||
|
QLibraryInfo,
|
||||||
|
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__)))
|
||||||
@@ -82,10 +91,12 @@ if platform.system() == "Darwin" and platform.processor() == "arm":
|
|||||||
|
|
||||||
# Custom message handler to filter out specific Qt warnings
|
# Custom message handler to filter out specific Qt warnings
|
||||||
def qt_message_handler(mode, context, message):
|
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:
|
if "Wayland does not support QWindow::requestActivate()" in message:
|
||||||
return # Suppress this specific message
|
return # Suppress this specific message
|
||||||
if "setGrabPopup called with a parent, QtWaylandClient" in message:
|
if "setGrabPopup called with a parent, QtWaylandClient" in message:
|
||||||
return
|
return
|
||||||
|
|
||||||
if mode == QtMsgType.QtWarningMsg:
|
if mode == QtMsgType.QtWarningMsg:
|
||||||
print(f"Qt Warning: {message}")
|
print(f"Qt Warning: {message}")
|
||||||
elif mode == QtMsgType.QtCriticalMsg:
|
elif mode == QtMsgType.QtCriticalMsg:
|
||||||
@@ -129,7 +140,7 @@ def main():
|
|||||||
|
|
||||||
ex = abogen()
|
ex = abogen()
|
||||||
ex.show()
|
ex.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+224
-101
@@ -2,7 +2,7 @@
|
|||||||
# button to remove an item from the queue
|
# button to remove an item from the queue
|
||||||
# button to clear the queue
|
# button to clear the queue
|
||||||
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
@@ -14,18 +14,20 @@ from PyQt5.QtWidgets import (
|
|||||||
QLabel,
|
QLabel,
|
||||||
QWidget,
|
QWidget,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
|
QAbstractItemView,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import QFileInfo, Qt
|
from PyQt6.QtCore import QFileInfo, Qt
|
||||||
from abogen.constants import COLORS
|
from abogen.constants import COLORS
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from PyQt5.QtGui import QFontMetrics
|
from PyQt6.QtGui import QFontMetrics
|
||||||
|
|
||||||
|
|
||||||
class ElidedLabel(QLabel):
|
class ElidedLabel(QLabel):
|
||||||
def __init__(self, text, parent=None):
|
def __init__(self, text):
|
||||||
super().__init__(text, parent)
|
super().__init__(text)
|
||||||
self._full_text = 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):
|
def setText(self, text):
|
||||||
self._full_text = text
|
self._full_text = text
|
||||||
@@ -34,7 +36,9 @@ class ElidedLabel(QLabel):
|
|||||||
|
|
||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
metrics = QFontMetrics(self.font())
|
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().setText(elided)
|
||||||
super().resizeEvent(event)
|
super().resizeEvent(event)
|
||||||
|
|
||||||
@@ -53,8 +57,12 @@ class QueueListItemWidget(QWidget):
|
|||||||
name_label = ElidedLabel(os.path.basename(file_name))
|
name_label = ElidedLabel(os.path.basename(file_name))
|
||||||
char_label = QLabel(f"Chars: {char_count}")
|
char_label = QLabel(f"Chars: {char_count}")
|
||||||
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
|
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
|
||||||
char_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
char_label.setAlignment(
|
||||||
char_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
|
char_label.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred
|
||||||
|
)
|
||||||
layout.addWidget(name_label, 1)
|
layout.addWidget(name_label, 1)
|
||||||
layout.addWidget(char_label, 0)
|
layout.addWidget(char_label, 0)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
@@ -67,12 +75,14 @@ class DroppableQueueListWidget(QListWidget):
|
|||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
# Overlay for drag hover
|
# Overlay for drag hover
|
||||||
self.drag_overlay = QLabel("", self)
|
self.drag_overlay = QLabel("", self)
|
||||||
self.drag_overlay.setAlignment(Qt.AlignCenter)
|
self.drag_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.drag_overlay.setStyleSheet(
|
self.drag_overlay.setStyleSheet(
|
||||||
f"border:2px dashed {COLORS['BLUE_BORDER_HOVER']}; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG_HOVER']};"
|
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.setVisible(False)
|
||||||
self.drag_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
|
self.drag_overlay.setAttribute(
|
||||||
|
Qt.WidgetAttribute.WA_TransparentForMouseEvents, True
|
||||||
|
)
|
||||||
|
|
||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
if event.mimeData().hasUrls():
|
if event.mimeData().hasUrls():
|
||||||
@@ -132,9 +142,11 @@ class QueueManager(QDialog):
|
|||||||
layout.setSpacing(12) # set spacing between widgets in main layout
|
layout.setSpacing(12) # set spacing between widgets in main layout
|
||||||
# list of queued items
|
# list of queued items
|
||||||
self.listwidget = DroppableQueueListWidget(self)
|
self.listwidget = DroppableQueueListWidget(self)
|
||||||
self.listwidget.setSelectionMode(QListWidget.ExtendedSelection)
|
self.listwidget.setSelectionMode(
|
||||||
|
QAbstractItemView.SelectionMode.ExtendedSelection
|
||||||
|
)
|
||||||
self.listwidget.setAlternatingRowColors(True)
|
self.listwidget.setAlternatingRowColors(True)
|
||||||
self.listwidget.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.listwidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
self.listwidget.customContextMenuRequested.connect(self.show_context_menu)
|
self.listwidget.customContextMenuRequested.connect(self.show_context_menu)
|
||||||
# Add informative instructions at the top
|
# Add informative instructions at the top
|
||||||
instructions = QLabel(
|
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. "
|
"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."
|
"You can view each file's configuration by hovering over them."
|
||||||
)
|
)
|
||||||
instructions.setAlignment(Qt.AlignLeft)
|
instructions.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||||
instructions.setWordWrap(True)
|
instructions.setWordWrap(True)
|
||||||
instructions.setStyleSheet("margin-bottom: 8px;")
|
instructions.setStyleSheet("margin-bottom: 8px;")
|
||||||
layout.addWidget(instructions)
|
layout.addWidget(instructions)
|
||||||
@@ -154,12 +166,14 @@ class QueueManager(QDialog):
|
|||||||
"Drag and drop your text files here or use the 'Add files' button.",
|
"Drag and drop your text files here or use the 'Add files' button.",
|
||||||
self.listwidget,
|
self.listwidget,
|
||||||
)
|
)
|
||||||
self.empty_overlay.setAlignment(Qt.AlignCenter)
|
self.empty_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.empty_overlay.setStyleSheet(
|
self.empty_overlay.setStyleSheet(
|
||||||
f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;"
|
f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;"
|
||||||
)
|
)
|
||||||
self.empty_overlay.setWordWrap(True)
|
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()
|
self.empty_overlay.hide()
|
||||||
# add queue items to the list
|
# add queue items to the list
|
||||||
self.process_queue()
|
self.process_queue()
|
||||||
@@ -192,7 +206,10 @@ class QueueManager(QDialog):
|
|||||||
self.listwidget.currentItemChanged.connect(self.update_button_states)
|
self.listwidget.currentItemChanged.connect(self.update_button_states)
|
||||||
self.listwidget.itemSelectionChanged.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.accepted.connect(self.accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
|
|
||||||
@@ -211,7 +228,6 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
self.listwidget.clear()
|
self.listwidget.clear()
|
||||||
if not self.queue:
|
if not self.queue:
|
||||||
self.empty_overlay.resize(self.listwidget.size())
|
|
||||||
self.empty_overlay.show()
|
self.empty_overlay.show()
|
||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
return
|
return
|
||||||
@@ -224,8 +240,16 @@ class QueueManager(QDialog):
|
|||||||
processing_file_path = item.file_name
|
processing_file_path = item.file_name
|
||||||
|
|
||||||
# Normalize paths for consistent display (fixes Windows path separator issues)
|
# 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
|
display_file_path = (
|
||||||
processing_file_path = os.path.normpath(processing_file_path) if processing_file_path else processing_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
|
# Only show the file name, not the full path
|
||||||
display_name = display_file_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
|
# For plain .txt inputs we don't need to show a separate processing file
|
||||||
show_processing = True
|
show_processing = True
|
||||||
try:
|
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
|
show_processing = False
|
||||||
except Exception:
|
except Exception:
|
||||||
show_processing = True
|
show_processing = True
|
||||||
|
|
||||||
tooltip = f"<b>Input File:</b> {display_file_path}<br>"
|
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>Processing File:</b> {processing_file_path}<br>"
|
||||||
tooltip += (
|
tooltip += (
|
||||||
f"<b>Language:</b> {getattr(item, 'lang_code', '')}<br>"
|
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>Characters:</b> {getattr(item, 'total_char_count', '')}<br>"
|
||||||
f"<b>Replace Single Newlines:</b> {getattr(item, 'replace_single_newlines', False)}"
|
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.setToolTip(tooltip)
|
||||||
list_item.setIcon(icon)
|
list_item.setIcon(icon)
|
||||||
# Store both paths for context menu
|
# Store both paths for context menu
|
||||||
list_item.setData(Qt.UserRole, {
|
list_item.setData(
|
||||||
'display_path': display_file_path,
|
Qt.ItemDataRole.UserRole,
|
||||||
'processing_path': processing_file_path
|
{
|
||||||
})
|
"display_path": display_file_path,
|
||||||
|
"processing_path": processing_file_path,
|
||||||
|
},
|
||||||
|
)
|
||||||
# Use custom widget for display
|
# Use custom widget for display
|
||||||
char_count = getattr(item, "total_char_count", 0)
|
char_count = getattr(item, "total_char_count", 0)
|
||||||
widget = QueueListItemWidget(display_file_path, char_count)
|
widget = QueueListItemWidget(display_file_path, char_count)
|
||||||
@@ -280,7 +321,7 @@ class QueueManager(QDialog):
|
|||||||
items = self.listwidget.selectedItems()
|
items = self.listwidget.selectedItems()
|
||||||
if not items:
|
if not items:
|
||||||
return
|
return
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
# Remove by index to ensure correct mapping
|
# Remove by index to ensure correct mapping
|
||||||
rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
|
rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
|
||||||
@@ -290,10 +331,10 @@ class QueueManager(QDialog):
|
|||||||
self,
|
self,
|
||||||
"Confirm Remove",
|
"Confirm Remove",
|
||||||
f"Are you sure you want to remove {len(rows)} 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.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
QMessageBox.No,
|
QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if 0 <= row < len(self.queue):
|
if 0 <= row < len(self.queue):
|
||||||
@@ -302,17 +343,17 @@ class QueueManager(QDialog):
|
|||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
|
|
||||||
def clear_queue(self):
|
def clear_queue(self):
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
if len(self.queue) > 1:
|
if len(self.queue) > 1:
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Confirm Clear Queue",
|
"Confirm Clear Queue",
|
||||||
f"Are you sure you want to clear {len(self.queue)} items from the queue?",
|
f"Are you sure you want to clear {len(self.queue)} items from the queue?",
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
QMessageBox.No,
|
QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
self.queue.clear()
|
self.queue.clear()
|
||||||
self.listwidget.clear()
|
self.listwidget.clear()
|
||||||
@@ -362,6 +403,13 @@ class QueueManager(QDialog):
|
|||||||
attrs["replace_single_newlines"] = getattr(
|
attrs["replace_single_newlines"] = getattr(
|
||||||
parent, "replace_single_newlines", False
|
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:
|
else:
|
||||||
# fallback: empty values
|
# fallback: empty values
|
||||||
attrs = {
|
attrs = {
|
||||||
@@ -378,11 +426,13 @@ class QueueManager(QDialog):
|
|||||||
"replace_single_newlines",
|
"replace_single_newlines",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
attrs["save_chapters_separately"] = None
|
||||||
|
attrs["merge_chapters_at_end"] = None
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def add_files_from_paths(self, file_paths):
|
def add_files_from_paths(self, file_paths):
|
||||||
from abogen.utils import calculate_text_length
|
from abogen.utils import calculate_text_length
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
import os
|
import os
|
||||||
|
|
||||||
current_attrs = self.get_current_attributes()
|
current_attrs = self.get_current_attributes()
|
||||||
@@ -394,7 +444,9 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
item = QueueItem()
|
item = QueueItem()
|
||||||
item.file_name = file_path
|
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():
|
for attr, value in current_attrs.items():
|
||||||
setattr(item, attr, value)
|
setattr(item, attr, value)
|
||||||
# Read file content and calculate total_char_count using calculate_text_length
|
# 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)
|
== getattr(item, "replace_single_newlines", False)
|
||||||
and getattr(queued_item, "save_base_path", None)
|
and getattr(queued_item, "save_base_path", None)
|
||||||
== getattr(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
|
is_duplicate = True
|
||||||
break
|
break
|
||||||
@@ -447,7 +503,7 @@ 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
|
from PyQt6.QtWidgets import QFileDialog
|
||||||
from abogen.utils import calculate_text_length # import the function
|
from abogen.utils import calculate_text_length # import the function
|
||||||
|
|
||||||
# Only allow .txt files
|
# Only allow .txt files
|
||||||
@@ -477,9 +533,9 @@ class QueueManager(QDialog):
|
|||||||
self.clear_button.setEnabled(bool(self.queue))
|
self.clear_button.setEnabled(bool(self.queue))
|
||||||
|
|
||||||
def show_context_menu(self, pos):
|
def show_context_menu(self, pos):
|
||||||
from PyQt5.QtWidgets import QMenu, QAction
|
from PyQt6.QtWidgets import QMenu
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt6.QtGui import QAction, QDesktopServices
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt6.QtCore import QUrl
|
||||||
import os
|
import os
|
||||||
|
|
||||||
global_pos = self.listwidget.viewport().mapToGlobal(pos)
|
global_pos = self.listwidget.viewport().mapToGlobal(pos)
|
||||||
@@ -491,75 +547,129 @@ class QueueManager(QDialog):
|
|||||||
remove_action.triggered.connect(self.remove_item)
|
remove_action.triggered.connect(self.remove_item)
|
||||||
menu.addAction(remove_action)
|
menu.addAction(remove_action)
|
||||||
|
|
||||||
# Add Open file action
|
# Get paths for determining if it's a document input
|
||||||
open_file_action = QAction("Open file", self)
|
|
||||||
|
|
||||||
def open_file():
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
item = selected_items[0]
|
|
||||||
paths = item.data(Qt.UserRole)
|
|
||||||
if isinstance(paths, dict):
|
|
||||||
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 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)
|
|
||||||
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]
|
item = selected_items[0]
|
||||||
paths = item.data(Qt.UserRole)
|
paths = item.data(Qt.ItemDataRole.UserRole)
|
||||||
if isinstance(paths, dict):
|
if isinstance(paths, dict):
|
||||||
display_path = paths.get('display_path', '')
|
display_path = paths.get("display_path", "")
|
||||||
processing_path = paths.get('processing_path', '')
|
processing_path = paths.get("processing_path", "")
|
||||||
else:
|
else:
|
||||||
display_path = paths
|
display_path = paths
|
||||||
processing_path = paths
|
processing_path = paths
|
||||||
|
|
||||||
doc_exts = ('.md', '.markdown', '.pdf', '.epub')
|
doc_exts = (".md", ".markdown", ".pdf", ".epub")
|
||||||
is_document_input = (
|
is_document_input = (
|
||||||
isinstance(display_path, str) and display_path.lower().endswith(doc_exts)
|
isinstance(display_path, str)
|
||||||
|
and display_path.lower().endswith(doc_exts)
|
||||||
) or (
|
) or (
|
||||||
isinstance(processing_path, str) and processing_path.lower().endswith(doc_exts)
|
isinstance(processing_path, str)
|
||||||
|
and processing_path.lower().endswith(doc_exts)
|
||||||
)
|
)
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
# 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) == 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))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
def open_folder_for(path_label: str):
|
def open_folder_for(path_label: str):
|
||||||
# path_label should be either 'display' or 'processing'
|
# 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:
|
if not p:
|
||||||
QMessageBox.warning(self, "File Not Found", "Path is not available.")
|
QMessageBox.warning(
|
||||||
|
self, "File Not Found", "Path is not available."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
# If the stored path is the display path (original) but the actual file may be
|
# 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.
|
# stored on the queue object differently, try to resolve via the queue entry.
|
||||||
target_path = None
|
target_path = None
|
||||||
for q in self.queue:
|
for q in self.queue:
|
||||||
if getattr(q, 'save_base_path', None) == display_path or q.file_name == display_path:
|
if (
|
||||||
if path_label == 'display':
|
getattr(q, "save_base_path", None) == display_path
|
||||||
target_path = getattr(q, 'save_base_path', None) or q.file_name
|
or q.file_name == display_path
|
||||||
|
):
|
||||||
|
if path_label == "display":
|
||||||
|
target_path = (
|
||||||
|
getattr(q, "save_base_path", None) or q.file_name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
target_path = q.file_name
|
target_path = q.file_name
|
||||||
break
|
break
|
||||||
if getattr(q, 'save_base_path', None) == processing_path or q.file_name == processing_path:
|
if (
|
||||||
if path_label == 'display':
|
getattr(q, "save_base_path", None) == processing_path
|
||||||
target_path = getattr(q, 'save_base_path', None) or q.file_name
|
or q.file_name == processing_path
|
||||||
|
):
|
||||||
|
if path_label == "display":
|
||||||
|
target_path = (
|
||||||
|
getattr(q, "save_base_path", None) or q.file_name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
target_path = q.file_name
|
target_path = q.file_name
|
||||||
break
|
break
|
||||||
@@ -568,7 +678,11 @@ class QueueManager(QDialog):
|
|||||||
target_path = p
|
target_path = p
|
||||||
|
|
||||||
if not os.path.exists(target_path):
|
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
|
return
|
||||||
folder = os.path.dirname(target_path)
|
folder = os.path.dirname(target_path)
|
||||||
if os.path.exists(folder):
|
if os.path.exists(folder):
|
||||||
@@ -576,11 +690,13 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
if is_document_input:
|
if is_document_input:
|
||||||
processed_action = QAction("Go to processed file", self)
|
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)
|
menu.addAction(processed_action)
|
||||||
|
|
||||||
input_action = QAction("Go to input file", self)
|
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)
|
menu.addAction(input_action)
|
||||||
else:
|
else:
|
||||||
# Default behavior for non-document inputs: single "Go to folder" action
|
# Default behavior for non-document inputs: single "Go to folder" action
|
||||||
@@ -588,15 +704,22 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
def go_to_folder():
|
def go_to_folder():
|
||||||
item = selected_items[0]
|
item = selected_items[0]
|
||||||
paths = item.data(Qt.UserRole)
|
paths = item.data(Qt.ItemDataRole.UserRole)
|
||||||
if isinstance(paths, dict):
|
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:
|
else:
|
||||||
file_path = paths # Fallback for old format
|
file_path = paths # Fallback for old format
|
||||||
# Find the queue item
|
# Find the queue item
|
||||||
for q in self.queue:
|
for q in self.queue:
|
||||||
if (getattr(q, "save_base_path", None) == file_path or q.file_name == file_path):
|
if (
|
||||||
target_path = getattr(q, "save_base_path", None) or q.file_name
|
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):
|
if not os.path.exists(target_path):
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, "File Not Found", f"The file does not exist."
|
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 = QAction("Clear Queue", self)
|
||||||
clear_action.triggered.connect(self.clear_queue)
|
clear_action.triggered.connect(self.clear_queue)
|
||||||
menu.addAction(clear_action)
|
menu.addAction(clear_action)
|
||||||
menu.exec_(global_pos)
|
menu.exec(global_pos)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
# Accept: keep changes
|
# Accept: keep changes
|
||||||
@@ -626,7 +749,7 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
def reject(self):
|
def reject(self):
|
||||||
# Cancel: restore original queue
|
# 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)
|
# Warn if user changed a lot (e.g., more than 1 items difference)
|
||||||
original_count = len(self._original_queue)
|
original_count = len(self._original_queue)
|
||||||
@@ -636,19 +759,19 @@ class QueueManager(QDialog):
|
|||||||
self,
|
self,
|
||||||
"Confirm Cancel",
|
"Confirm Cancel",
|
||||||
f"Are you sure you want to cancel and discard all changes?",
|
f"Are you sure you want to cancel and discard all changes?",
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
QMessageBox.No,
|
QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
self.queue.clear()
|
self.queue.clear()
|
||||||
self.queue.extend(deepcopy(self._original_queue))
|
self.queue.extend(deepcopy(self._original_queue))
|
||||||
super().reject()
|
super().reject()
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
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()
|
self.remove_item()
|
||||||
else:
|
else:
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ class QueuedItem:
|
|||||||
total_char_count: int
|
total_char_count: int
|
||||||
replace_single_newlines: bool = False
|
replace_single_newlines: bool = False
|
||||||
save_base_path: str = None
|
save_base_path: str = None
|
||||||
|
save_chapters_separately: bool = None
|
||||||
|
merge_chapters_at_end: bool = None
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ warnings.filterwarnings("ignore")
|
|||||||
def detect_encoding(file_path):
|
def detect_encoding(file_path):
|
||||||
import chardet
|
import chardet
|
||||||
import charset_normalizer
|
import charset_normalizer
|
||||||
|
|
||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
raw_data = f.read()
|
raw_data = f.read()
|
||||||
detected_encoding = None
|
detected_encoding = None
|
||||||
@@ -28,6 +29,7 @@ def detect_encoding(file_path):
|
|||||||
encoding = detected_encoding if detected_encoding else "utf-8"
|
encoding = detected_encoding if detected_encoding else "utf-8"
|
||||||
return encoding.lower()
|
return encoding.lower()
|
||||||
|
|
||||||
|
|
||||||
def get_resource_path(package, resource):
|
def get_resource_path(package, resource):
|
||||||
"""
|
"""
|
||||||
Get the path to a resource file, with fallback to local file system.
|
Get the path to a resource file, with fallback to local file system.
|
||||||
|
|||||||
+149
-76
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
@@ -22,12 +22,11 @@ from PyQt5.QtWidgets import (
|
|||||||
QFileDialog,
|
QFileDialog,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
QMenu,
|
QMenu,
|
||||||
QAction,
|
|
||||||
QComboBox,
|
|
||||||
QApplication,
|
QApplication,
|
||||||
|
QComboBox,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize
|
from PyQt6.QtCore import Qt, QTimer, QPoint, QRect, QSize
|
||||||
from PyQt5.QtGui import QPixmap, QIcon
|
from PyQt6.QtGui import QPixmap, QIcon, QAction
|
||||||
from abogen.constants import (
|
from abogen.constants import (
|
||||||
VOICES_INTERNAL,
|
VOICES_INTERNAL,
|
||||||
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
@@ -91,7 +90,7 @@ class FlowLayout(QLayout):
|
|||||||
return len(self._item_list)
|
return len(self._item_list)
|
||||||
|
|
||||||
def expandingDirections(self):
|
def expandingDirections(self):
|
||||||
return Qt.Orientations(Qt.Orientation(0))
|
return Qt.Orientation(0)
|
||||||
|
|
||||||
def hasHeightForWidth(self):
|
def hasHeightForWidth(self):
|
||||||
return True
|
return True
|
||||||
@@ -132,10 +131,14 @@ class FlowLayout(QLayout):
|
|||||||
for item in self._item_list:
|
for item in self._item_list:
|
||||||
style = self.parentWidget().style() if self.parentWidget() else QStyle()
|
style = self.parentWidget().style() if self.parentWidget() else QStyle()
|
||||||
layout_spacing_x = style.layoutSpacing(
|
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(
|
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_x = spacing if spacing >= 0 else layout_spacing_x
|
||||||
space_y = spacing if spacing >= 0 else layout_spacing_y
|
space_y = spacing if spacing >= 0 else layout_spacing_y
|
||||||
@@ -163,7 +166,7 @@ class VoiceMixer(QWidget):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.voice_name = voice_name
|
self.voice_name = voice_name
|
||||||
self.setFixedWidth(VOICE_MIXER_WIDTH)
|
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
|
# TODO Set CSS for rounded corners
|
||||||
# self.setObjectName("VoiceMixer")
|
# self.setObjectName("VoiceMixer")
|
||||||
@@ -173,7 +176,7 @@ class VoiceMixer(QWidget):
|
|||||||
|
|
||||||
# Name label at the top
|
# Name label at the top
|
||||||
name = voice_name
|
name = voice_name
|
||||||
layout.addWidget(QLabel(name), alignment=Qt.AlignCenter)
|
layout.addWidget(QLabel(name), alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
# Voice name label with gender icon
|
# Voice name label with gender icon
|
||||||
is_female = self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f"
|
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 (flag and gender)
|
||||||
icons_layout = QHBoxLayout()
|
icons_layout = QHBoxLayout()
|
||||||
icons_layout.setSpacing(3)
|
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
|
||||||
flag_icon_path = get_resource_path(
|
flag_icon_path = get_resource_path(
|
||||||
@@ -194,11 +199,21 @@ class VoiceMixer(QWidget):
|
|||||||
gender_label = QLabel()
|
gender_label = QLabel()
|
||||||
flag_pixmap = QPixmap(flag_icon_path)
|
flag_pixmap = QPixmap(flag_icon_path)
|
||||||
flag_label.setPixmap(
|
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_pixmap = QPixmap(gender_icon_path)
|
||||||
gender_label.setPixmap(
|
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(flag_label)
|
||||||
icons_layout.addWidget(gender_label)
|
icons_layout.addWidget(gender_label)
|
||||||
@@ -210,7 +225,7 @@ class VoiceMixer(QWidget):
|
|||||||
self.checkbox = QCheckBox()
|
self.checkbox = QCheckBox()
|
||||||
self.checkbox.setChecked(initial_status)
|
self.checkbox.setChecked(initial_status)
|
||||||
self.checkbox.stateChanged.connect(self.toggle_inputs)
|
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
|
# Spinbox and slider
|
||||||
self.spin_box = QDoubleSpinBox()
|
self.spin_box = QDoubleSpinBox()
|
||||||
@@ -219,26 +234,16 @@ class VoiceMixer(QWidget):
|
|||||||
self.spin_box.setDecimals(2)
|
self.spin_box.setDecimals(2)
|
||||||
self.spin_box.setValue(initial_weight)
|
self.spin_box.setValue(initial_weight)
|
||||||
|
|
||||||
self.slider = QSlider(Qt.Vertical)
|
self.slider = QSlider(Qt.Orientation.Vertical)
|
||||||
self.slider.setRange(0, 100)
|
self.slider.setRange(0, 100)
|
||||||
self.slider.setValue(int(initial_weight * 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)
|
self.slider.setFixedWidth(SLIDER_WIDTH)
|
||||||
|
|
||||||
# Fix slider in Windows
|
# Apply slider styling after widget is added to window (see showEvent)
|
||||||
if platform.system() == "Windows":
|
self._slider_style_applied = False
|
||||||
appstyle = QApplication.instance().style().objectName().lower()
|
|
||||||
if appstyle != "windowsvista":
|
|
||||||
# Set custom groove color for disabled state using COLORS["GREY_BACKGROUND"]
|
|
||||||
self.slider.setStyleSheet(
|
|
||||||
f"""
|
|
||||||
QSlider::groove:vertical:disabled {{
|
|
||||||
background: {COLORS.get("GREY_BACKGROUND")};
|
|
||||||
width: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
# Connect controls
|
# Connect controls
|
||||||
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
|
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
|
||||||
@@ -249,23 +254,67 @@ class VoiceMixer(QWidget):
|
|||||||
# Layout for slider and labels
|
# Layout for slider and labels
|
||||||
slider_layout = QVBoxLayout()
|
slider_layout = QVBoxLayout()
|
||||||
slider_layout.addWidget(self.spin_box)
|
slider_layout.addWidget(self.spin_box)
|
||||||
slider_layout.addWidget(QLabel("1", alignment=Qt.AlignCenter))
|
slider_layout.addWidget(QLabel("1", alignment=Qt.AlignmentFlag.AlignCenter))
|
||||||
|
|
||||||
slider_center_layout = QHBoxLayout()
|
slider_center_layout = QHBoxLayout()
|
||||||
slider_center_layout.addWidget(self.slider, alignment=Qt.AlignHCenter)
|
slider_center_layout.addWidget(
|
||||||
|
self.slider, alignment=Qt.AlignmentFlag.AlignHCenter
|
||||||
|
)
|
||||||
slider_center_layout.setContentsMargins(0, 0, 0, 0)
|
slider_center_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
slider_center_widget = QWidget()
|
slider_center_widget = QWidget()
|
||||||
slider_center_widget.setLayout(slider_center_layout)
|
slider_center_widget.setLayout(slider_center_layout)
|
||||||
|
|
||||||
slider_layout.addWidget(slider_center_widget, stretch=1)
|
slider_layout.addWidget(slider_center_widget, stretch=1)
|
||||||
slider_layout.addWidget(QLabel("0", alignment=Qt.AlignCenter))
|
slider_layout.addWidget(QLabel("0", alignment=Qt.AlignmentFlag.AlignCenter))
|
||||||
slider_layout.setStretch(2, 1)
|
slider_layout.setStretch(2, 1)
|
||||||
|
|
||||||
layout.addLayout(slider_layout, stretch=1)
|
layout.addLayout(slider_layout, stretch=1)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
self.toggle_inputs()
|
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()
|
||||||
|
if appstyle != "windowsvista":
|
||||||
|
# Set custom groove color for disabled state using COLORS["GREY_BACKGROUND"]
|
||||||
|
self.slider.setStyleSheet(
|
||||||
|
f"""
|
||||||
|
QSlider::groove:vertical:disabled {{
|
||||||
|
background: {COLORS.get("GREY_BACKGROUND")};
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
|
||||||
|
if theme == "light":
|
||||||
|
self.slider.setStyleSheet(
|
||||||
|
f"""
|
||||||
|
QSlider::groove:vertical:disabled {{
|
||||||
|
background: {COLORS.get("GREY_BACKGROUND")};
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
def toggle_inputs(self):
|
def toggle_inputs(self):
|
||||||
is_enabled = self.checkbox.isChecked()
|
is_enabled = self.checkbox.isChecked()
|
||||||
self.spin_box.setEnabled(is_enabled)
|
self.spin_box.setEnabled(is_enabled)
|
||||||
@@ -307,9 +356,11 @@ class HoverLabel(QLabel):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
# Make sure the entire button is clickable, not just the text
|
# Make sure the entire button is clickable, not just the text
|
||||||
self.delete_button.setFocusPolicy(Qt.NoFocus)
|
self.delete_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
self.delete_button.setAttribute(Qt.WA_TransparentForMouseEvents, False)
|
self.delete_button.setAttribute(
|
||||||
self.delete_button.setCursor(Qt.PointingHandCursor)
|
Qt.WidgetAttribute.WA_TransparentForMouseEvents, False
|
||||||
|
)
|
||||||
|
self.delete_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
self.delete_button.hide()
|
self.delete_button.hide()
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
@@ -357,7 +408,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
if parent is not None and hasattr(parent, "subtitle_combo"):
|
if parent is not None and hasattr(parent, "subtitle_combo"):
|
||||||
self.subtitle_combo = parent.subtitle_combo
|
self.subtitle_combo = parent.subtitle_combo
|
||||||
# Create main container layout with profile section and mixer section
|
# Create main container layout with profile section and mixer section
|
||||||
splitter = QSplitter(Qt.Horizontal)
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||||
# Profile section
|
# Profile section
|
||||||
profile_widget = QWidget()
|
profile_widget = QWidget()
|
||||||
profile_layout = QVBoxLayout(profile_widget)
|
profile_layout = QVBoxLayout(profile_widget)
|
||||||
@@ -371,8 +422,8 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
profile_layout.addLayout(header_layout)
|
profile_layout.addLayout(header_layout)
|
||||||
# Profile list
|
# Profile list
|
||||||
self.profile_list = QListWidget()
|
self.profile_list = QListWidget()
|
||||||
self.profile_list.setSelectionMode(QListWidget.SingleSelection)
|
self.profile_list.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
|
||||||
self.profile_list.setSelectionBehavior(QListWidget.SelectRows)
|
self.profile_list.setSelectionBehavior(QListWidget.SelectionBehavior.SelectRows)
|
||||||
self.profile_list.setStyleSheet(
|
self.profile_list.setStyleSheet(
|
||||||
"QListWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }"
|
"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)
|
idx = list(profiles.keys()).index(self.current_profile)
|
||||||
self.profile_list.setCurrentRow(idx)
|
self.profile_list.setCurrentRow(idx)
|
||||||
profile_layout.addWidget(self.profile_list)
|
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.profile_list.customContextMenuRequested.connect(
|
||||||
self.show_profile_context_menu
|
self.show_profile_context_menu
|
||||||
)
|
)
|
||||||
@@ -409,7 +460,9 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
|
|
||||||
self.setWindowTitle("Voice Mixer")
|
self.setWindowTitle("Voice Mixer")
|
||||||
self.setWindowFlags(
|
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.setMinimumSize(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||||
self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)
|
self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT)
|
||||||
@@ -468,21 +521,25 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
separator = QFrame()
|
separator = QFrame()
|
||||||
separator.setFrameShadow(QFrame.Sunken)
|
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
||||||
mixer_layout.addWidget(separator)
|
mixer_layout.addWidget(separator)
|
||||||
|
|
||||||
# Voice list scroll area
|
# Voice list scroll area
|
||||||
self.scroll_area = QScrollArea()
|
self.scroll_area = QScrollArea()
|
||||||
self.scroll_area.setWidgetResizable(True)
|
self.scroll_area.setWidgetResizable(True)
|
||||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
self.scroll_area.setHorizontalScrollBarPolicy(
|
||||||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
Qt.ScrollBarPolicy.ScrollBarAsNeeded
|
||||||
|
)
|
||||||
|
self.scroll_area.setVerticalScrollBarPolicy(
|
||||||
|
Qt.ScrollBarPolicy.ScrollBarAsNeeded
|
||||||
|
)
|
||||||
self.scroll_area.viewport().installEventFilter(self)
|
self.scroll_area.viewport().installEventFilter(self)
|
||||||
|
|
||||||
self.voice_list_widget = QWidget()
|
self.voice_list_widget = QWidget()
|
||||||
self.voice_list_layout = QHBoxLayout()
|
self.voice_list_layout = QHBoxLayout()
|
||||||
self.voice_list_widget.setLayout(self.voice_list_layout)
|
self.voice_list_widget.setLayout(self.voice_list_layout)
|
||||||
self.voice_list_widget.setSizePolicy(
|
self.voice_list_widget.setSizePolicy(
|
||||||
QSizePolicy.Expanding, QSizePolicy.Expanding
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
||||||
)
|
)
|
||||||
self.scroll_area.setWidget(self.voice_list_widget)
|
self.scroll_area.setWidget(self.voice_list_widget)
|
||||||
mixer_layout.addWidget(self.scroll_area, stretch=1)
|
mixer_layout.addWidget(self.scroll_area, stretch=1)
|
||||||
@@ -497,6 +554,9 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
ok_button.setDefault(True)
|
ok_button.setDefault(True)
|
||||||
ok_button.setFocus()
|
ok_button.setFocus()
|
||||||
|
|
||||||
|
# Connect buttons
|
||||||
|
clear_all_button.clicked.connect(self.clear_all_voices)
|
||||||
|
ok_button.clicked.connect(self.accept)
|
||||||
# Connect buttons
|
# Connect buttons
|
||||||
clear_all_button.clicked.connect(self.clear_all_voices)
|
clear_all_button.clicked.connect(self.clear_all_voices)
|
||||||
ok_button.clicked.connect(self.accept)
|
ok_button.clicked.connect(self.accept)
|
||||||
@@ -533,7 +593,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
# Bind Delete key to delete_profile when a profile is selected
|
# 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()
|
item = self.profile_list.currentItem()
|
||||||
if item:
|
if item:
|
||||||
self.delete_profile(item)
|
self.delete_profile(item)
|
||||||
@@ -562,10 +622,12 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self,
|
self,
|
||||||
"Unsaved Changes",
|
"Unsaved Changes",
|
||||||
msg,
|
msg,
|
||||||
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
|
QMessageBox.StandardButton.Save
|
||||||
QMessageBox.Save,
|
| QMessageBox.StandardButton.Discard
|
||||||
|
| QMessageBox.StandardButton.Cancel,
|
||||||
|
QMessageBox.StandardButton.Save,
|
||||||
)
|
)
|
||||||
if ret == QMessageBox.Save:
|
if ret == QMessageBox.StandardButton.Save:
|
||||||
# Save all using stored states
|
# Save all using stored states
|
||||||
profiles = load_profiles()
|
profiles = load_profiles()
|
||||||
for i in dirty_indices:
|
for i in dirty_indices:
|
||||||
@@ -590,7 +652,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self.update_profile_save_buttons()
|
self.update_profile_save_buttons()
|
||||||
self.update_profile_list_colors()
|
self.update_profile_list_colors()
|
||||||
return True
|
return True
|
||||||
elif ret == QMessageBox.Discard:
|
elif ret == QMessageBox.StandardButton.Discard:
|
||||||
# Discard all modifications
|
# Discard all modifications
|
||||||
self._profile_states.clear()
|
self._profile_states.clear()
|
||||||
for i in dirty_indices:
|
for i in dirty_indices:
|
||||||
@@ -612,17 +674,19 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
else:
|
else:
|
||||||
# Fallback to original logic for 0 or 1 dirty profile
|
# Fallback to original logic for 0 or 1 dirty profile
|
||||||
box = QMessageBox(self)
|
box = QMessageBox(self)
|
||||||
box.setIcon(QMessageBox.Warning)
|
box.setIcon(QMessageBox.Icon.Warning)
|
||||||
box.setWindowTitle("Unsaved Changes")
|
box.setWindowTitle("Unsaved Changes")
|
||||||
box.setText(
|
box.setText(
|
||||||
"You have unsaved changes in your profile. Do you want to save the changes?"
|
"You have unsaved changes in your profile. Do you want to save the changes?"
|
||||||
)
|
)
|
||||||
box.setStandardButtons(
|
box.setStandardButtons(
|
||||||
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
|
QMessageBox.StandardButton.Save
|
||||||
|
| QMessageBox.StandardButton.Discard
|
||||||
|
| QMessageBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
box.setDefaultButton(QMessageBox.Save)
|
box.setDefaultButton(QMessageBox.StandardButton.Save)
|
||||||
ret = box.exec_()
|
ret = box.exec()
|
||||||
if ret == QMessageBox.Save:
|
if ret == QMessageBox.StandardButton.Save:
|
||||||
for i in range(self.profile_list.count()):
|
for i in range(self.profile_list.count()):
|
||||||
item = self.profile_list.item(i)
|
item = self.profile_list.item(i)
|
||||||
name = item.text().lstrip("*")
|
name = item.text().lstrip("*")
|
||||||
@@ -636,7 +700,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
if hasattr(parent, "populate_profiles_in_voice_combo"):
|
if hasattr(parent, "populate_profiles_in_voice_combo"):
|
||||||
parent.populate_profiles_in_voice_combo()
|
parent.populate_profiles_in_voice_combo()
|
||||||
return True
|
return True
|
||||||
elif ret == QMessageBox.Discard:
|
elif ret == QMessageBox.StandardButton.Discard:
|
||||||
profiles = load_profiles()
|
profiles = load_profiles()
|
||||||
for i in range(self.profile_list.count()):
|
for i in range(self.profile_list.count()):
|
||||||
item = self.profile_list.item(i)
|
item = self.profile_list.item(i)
|
||||||
@@ -717,7 +781,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
return voice_mixer
|
return voice_mixer
|
||||||
|
|
||||||
def handle_voice_checkbox(self, voice_mixer, state):
|
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.last_enabled_voice = voice_mixer.voice_name
|
||||||
self.update_weighted_sums()
|
self.update_weighted_sums()
|
||||||
|
|
||||||
@@ -767,7 +831,9 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
f'<b><span style="color:{COLORS.get("BLUE")}">{name}: {percentage:.1f}%</span></b>',
|
f'<b><span style="color:{COLORS.get("BLUE")}">{name}: {percentage:.1f}%</span></b>',
|
||||||
name,
|
name,
|
||||||
)
|
)
|
||||||
voice_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
voice_label.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred
|
||||||
|
)
|
||||||
voice_label.delete_button.clicked.connect(
|
voice_label.delete_button.clicked.connect(
|
||||||
lambda _, vn=name: self.disable_voice_by_name(vn)
|
lambda _, vn=name: self.disable_voice_by_name(vn)
|
||||||
)
|
)
|
||||||
@@ -787,7 +853,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
mixer.checkbox.setChecked(False)
|
mixer.checkbox.setChecked(False)
|
||||||
|
|
||||||
def eventFilter(self, source, event):
|
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
|
# Skip if over an enabled slider
|
||||||
if any(
|
if any(
|
||||||
mixer.slider.underMouse() and mixer.slider.isEnabled()
|
mixer.slider.underMouse() and mixer.slider.isEnabled()
|
||||||
@@ -890,10 +956,10 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self,
|
self,
|
||||||
"Invalid Profiles",
|
"Invalid Profiles",
|
||||||
msg,
|
msg,
|
||||||
QMessageBox.Yes | QMessageBox.Cancel,
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
||||||
QMessageBox.Yes,
|
QMessageBox.StandardButton.Yes,
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
for i, name in reversed(zero):
|
for i, name in reversed(zero):
|
||||||
self.profile_list.takeItem(i)
|
self.profile_list.takeItem(i)
|
||||||
delete_profile(name)
|
delete_profile(name)
|
||||||
@@ -980,8 +1046,8 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
def _parse_rgba_to_qcolor(self, rgba_str):
|
def _parse_rgba_to_qcolor(self, rgba_str):
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt5.QtGui import QColor
|
from PyQt6.QtGui import QColor
|
||||||
|
|
||||||
"""Helper to convert 'rgba(R,G,B,A_float)' string to 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)
|
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 += f"\nThis will overwrite an existing profile."
|
||||||
msg += "\nContinue?"
|
msg += "\nContinue?"
|
||||||
reply = QMessageBox.question(
|
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
|
return
|
||||||
profiles.update(imported_profiles)
|
profiles.update(imported_profiles)
|
||||||
save_profiles(profiles)
|
save_profiles(profiles)
|
||||||
@@ -1153,9 +1222,12 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
msg += f"\n{len(collisions)} profile(s) will be overwritten."
|
msg += f"\n{len(collisions)} profile(s) will be overwritten."
|
||||||
msg += "\nContinue?"
|
msg += "\nContinue?"
|
||||||
reply = QMessageBox.question(
|
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
|
return
|
||||||
profiles.update(imported_profiles)
|
profiles.update(imported_profiles)
|
||||||
save_profiles(profiles)
|
save_profiles(profiles)
|
||||||
@@ -1196,7 +1268,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
menu.addAction(dup_act)
|
menu.addAction(dup_act)
|
||||||
menu.addAction(export_act)
|
menu.addAction(export_act)
|
||||||
menu.addAction(delete_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:
|
if act == rename_act:
|
||||||
self.rename_profile(item)
|
self.rename_profile(item)
|
||||||
elif act == delete_act:
|
elif act == delete_act:
|
||||||
@@ -1323,9 +1395,9 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self,
|
self,
|
||||||
"Delete Profile",
|
"Delete Profile",
|
||||||
f"Delete profile '{name}'?",
|
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)
|
delete_profile(name)
|
||||||
row = self.profile_list.row(item)
|
row = self.profile_list.row(item)
|
||||||
self.profile_list.takeItem(row)
|
self.profile_list.takeItem(row)
|
||||||
@@ -1378,7 +1450,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self.profile_list.setItemWidget(item, widget)
|
self.profile_list.setItemWidget(item, widget)
|
||||||
|
|
||||||
def update_profile_list_colors(self):
|
def update_profile_list_colors(self):
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
profiles = load_profiles()
|
profiles = load_profiles()
|
||||||
for i in range(self.profile_list.count()):
|
for i in range(self.profile_list.count()):
|
||||||
@@ -1386,13 +1458,14 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
name = item.text().lstrip("*")
|
name = item.text().lstrip("*")
|
||||||
if self._virtual_new_profile and name == "New profile":
|
if self._virtual_new_profile and name == "New profile":
|
||||||
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
|
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("*"):
|
elif item.text().startswith("*"):
|
||||||
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
|
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
|
||||||
item.setData(Qt.BackgroundRole, color)
|
item.setData(Qt.ItemDataRole.BackgroundRole, color)
|
||||||
else:
|
else:
|
||||||
item.setData(
|
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", [])
|
weights = profiles.get(name, {}).get("voices", [])
|
||||||
total = 0
|
total = 0
|
||||||
@@ -1406,7 +1479,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
total += entry[1]
|
total += entry[1]
|
||||||
if total == 0:
|
if total == 0:
|
||||||
color = self._parse_rgba_to_qcolor(COLORS.get("RED_BACKGROUND"))
|
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()
|
self.update_profile_save_buttons()
|
||||||
|
|
||||||
def preview_current_mix(self):
|
def preview_current_mix(self):
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ license = "MIT"
|
|||||||
requires-python = ">=3.10, <3.13"
|
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"]
|
keywords = ["audiobook", "epub", "pdf", "text-to-speech", "subtitle", "tts", "kokoro", "accessibility", "book-converter", "voice-synthesis", "multilingual", "chapter-management", "subtitles", "content-creation", "media-generation"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"PyQt5>=5.15.11",
|
"PyQt6>=6.10.0",
|
||||||
"kokoro>=0.9.4",
|
"kokoro>=0.9.4",
|
||||||
"misaki[zh]>=0.9.4",
|
"misaki[zh]>=0.9.4",
|
||||||
"ebooklib>=0.19",
|
"ebooklib>=0.19",
|
||||||
|
|||||||
Reference in New Issue
Block a user